class test_bug010(wttest.WiredTigerTestCase): name = 'test_bug010' uri = 'table:' + name num_tables = 2000 if wttest.islongtest() else 200 # Disable checkpoint sync, to make checkpoints faster and # increase the likelihood of triggering the symptom conn_config = 'checkpoint_sync=false' def test_checkpoint_dirty(self): # Create a lot of tables # insert the same item in each # Start a checkpoint with some of the updates # Create another checkpoint that should contain all data consistently # Read from the checkpoint and make sure the data is consistent for i in range(0, self.num_tables): self.printVerbose(3, 'Creating table ' + str(i)) self.session.create(self.uri + str(i), 'key_format=S,value_format=i') c = self.session.open_cursor(self.uri + str(i), None) c['a'] = 0 c.close() self.session.checkpoint() iterations = 1 expected_val = 0 for its in range(1, 10): self.printVerbose(3, 'Doing iteration ' + str(its)) # Create a checkpoint thread done = threading.Event() ckpt = wtthread.checkpoint_thread(self.conn, done) ckpt.start() try: expected_val += 1 for i in range(0, self.num_tables): c = self.session.open_cursor(self.uri + str(i), None) c['a'] = expected_val c.close() finally: done.set() ckpt.join() # Execute another checkpoint, to make sure we have a consistent # view of the data. self.session.checkpoint() for i in range(0, self.num_tables): c = self.session.open_cursor( self.uri + str(i), None, 'checkpoint=WiredTigerCheckpoint') c.next() self.assertEquals(c.get_value(), expected_val, msg='Mismatch on iteration ' + str(its) +\ ' for table ' + str(i)) c.close()
class test_backup02(wttest.WiredTigerTestCase): uri = 'table:test_backup02' fmt = 'L' dsize = 100 nops = 200 nthreads = 1 time = 60 if wttest.islongtest() else 10 def test_backup02(self): done = threading.Event() uris = list() uris.append(self.uri + str(1)) uris.append(self.uri + str(2)) uris.append(self.uri + str(3)) for this_uri in uris: self.session.create(this_uri, "key_format=" + self.fmt + ",value_format=S") # TODO: Ideally we'd like a checkpoint thread, separate to the backup # thread. Need a way to stop checkpoints while doing backups. # ckpt = checkpoint_thread(self.conn, done) # ckpt.start() bkp = backup_thread(self.conn, 'backup.dir', done) bkp.start() queue = Queue.Queue() my_data = 'a' * self.dsize for i in xrange(self.nops): queue.put_nowait(('gi', i, my_data)) opthreads = [] for i in xrange(self.nthreads): t = op_thread(self.conn, uris, self.fmt, queue, done) opthreads.append(t) t.start() # Add 200 update entries into the queue every .1 seconds. more_time = self.time while more_time > 0: time.sleep(0.1) my_data = str(more_time) + 'a' * (self.dsize - len(str(more_time))) more_time = more_time - 0.1 for i in xrange(self.nops): queue.put_nowait(('gu', i, my_data)) queue.join() done.set() # # Wait for checkpoint thread to notice status change. # ckpt.join() for t in opthreads: t.join() bkp.join()
class test_intpack(wttest.WiredTigerTestCase): name = 'test_intpack' # It's useful to test a larger range but avoid the CPU overhead normally base_range = 66000 if wttest.islongtest() else 5000 # We have to be a bit verbose here with naming, scenario names are # case insensitive and must be unique. scenarios = make_scenarios([ ('int8_t_b', dict(formatcode='b', low=-128, high=127, nbits=8)), ('uint8_t_B', dict(formatcode='B', low=0, high=255, nbits=8)), ('fix_len_8t', dict(formatcode='8t', low=0, high=255, nbits=8)), ('fix_len_5t', dict(formatcode='5t', low=0, high=31, nbits=5)), ('int16_t_h', dict(formatcode='h', low=-32768, high=32767, nbits=16)), ('uint16_t_H', dict(formatcode='H', low=0, high=65535, nbits=16)), ('int32_t_i', dict(formatcode='i', low=-2147483648, high=2147483647, nbits=32)), ('uint32_t_I', dict(formatcode='I', low=0, high=4294967295, nbits=32)), ('int32_t_l', dict(formatcode='l', low=-2147483648, high=2147483647, nbits=32)), ('uint32_t_L', dict(formatcode='L', low=0, high=4294967295, nbits=32)), ('int64_t_q', dict(formatcode='q', low=-9223372036854775808, high=9223372036854775807, nbits=64)), ('uint64_t_Q', dict(formatcode='Q', low=0, high=18446744073709551615, nbits=64)), ]) def test_packing(self): pt = PackTester(self.formatcode, self.low, self.high, self.assertEquals) self.assertEquals(2**self.nbits, self.high - self.low + 1) pt.initialize(self.session) pt.check_range(-self.base_range, self.base_range) if self.nbits >= 32: e32 = 2**32 pt.check_range(e32 - 1000, e32 + 1000) pt.check_range(-e32 - 1000, -e32 + 1000) if self.nbits >= 64: e64 = 2**64 pt.check_range(e64 - 1000, e64 + 1000) pt.check_range(-e64 - 1000, -e64 + 1000) pt.truncate() i = 8 while i < 1 << 60: pt.check_range(-i - 1, -i + 1) pt.check_range(i - 1, i + 1) i <<= 1
class test_txn02(wttest.WiredTigerTestCase, suite_subprocess): logmax = "100K" tablename = 'test_txn02' uri = 'table:' + tablename archive_list = ['true', 'false'] conn_list = ['reopen', 'stay_open'] sync_list = [ '(method=dsync,enabled)', '(method=fsync,enabled)', '(method=none,enabled)', '(enabled=false)' ] types = [ ('row', dict(tabletype='row', create_params='key_format=i,value_format=i')), ('var', dict(tabletype='var', create_params='key_format=r,value_format=i')), ('fix', dict(tabletype='fix', create_params='key_format=r,value_format=8t')), ] op1s = [ ('i4', dict(op1=('insert', 4))), ('r1', dict(op1=('remove', 1))), ('u10', dict(op1=('update', 10))), ] op2s = [ ('i6', dict(op2=('insert', 6))), ('r4', dict(op2=('remove', 4))), ('u4', dict(op2=('update', 4))), ] op3s = [ ('i12', dict(op3=('insert', 12))), ('r4', dict(op3=('remove', 4))), ('u4', dict(op3=('update', 4))), ] op4s = [ ('i14', dict(op4=('insert', 14))), ('r12', dict(op4=('remove', 12))), ('u12', dict(op4=('update', 12))), ] txn1s = [('t1c', dict(txn1='commit')), ('t1r', dict(txn1='rollback'))] txn2s = [('t2c', dict(txn2='commit')), ('t2r', dict(txn2='rollback'))] txn3s = [('t3c', dict(txn3='commit')), ('t3r', dict(txn3='rollback'))] txn4s = [('t4c', dict(txn4='commit')), ('t4r', dict(txn4='rollback'))] all_scenarios = multiply_scenarios('.', types, op1s, txn1s, op2s, txn2s, op3s, txn3s, op4s, txn4s) # This test generates thousands of potential scenarios. # For default runs, we'll use a small subset of them, for # long runs (when --long is set) we'll set a much larger limit. scenarios = number_scenarios(prune_scenarios(all_scenarios, 20, 5000)) # Each check_log() call takes a second, so we don't call it for # every scenario, we'll limit it to the value of checklog_calls. checklog_calls = 100 if wttest.islongtest() else 2 checklog_mod = (len(scenarios) / checklog_calls + 1) # scenarios = number_scenarios(multiply_scenarios('.', types, # op1s, txn1s, op2s, txn2s, op3s, txn3s, op4s, txn4s)) [:3] # Overrides WiredTigerTestCase def setUpConnectionOpen(self, dir): self.home = dir # Cycle through the different transaction_sync values in a # deterministic manner. self.txn_sync = self.sync_list[self.scenario_number % len(self.sync_list)] # # We don't want to run zero fill with only the same settings, such # as archive or sync, which are an even number of options. # freq = 3 zerofill = 'false' if self.scenario_number % freq == 0: zerofill = 'true' self.backup_dir = os.path.join(self.home, "WT_BACKUP") conn_params = \ 'log=(archive=false,enabled,file_max=%s),' % self.logmax + \ 'log=(zero_fill=%s),' % zerofill + \ 'create,error_prefix="%s: ",' % self.shortid() + \ 'transaction_sync="%s",' % self.txn_sync # print "Creating conn at '%s' with config '%s'" % (dir, conn_params) conn = wiredtiger_open(dir, conn_params) self.pr( ` conn `) self.session2 = conn.open_session() return conn # Check that a cursor (optionally started in a new transaction), sees the # expected values. def check(self, session, txn_config, expected): if txn_config: session.begin_transaction(txn_config) c = session.open_cursor(self.uri, None) actual = dict((k, v) for k, v in c if v != 0) # Search for the expected items as well as iterating for k, v in expected.iteritems(): self.assertEqual(c[k], v) c.close() if txn_config: session.commit_transaction() self.assertEqual(actual, expected) # Check the state of the system with respect to the current cursor and # different isolation levels. def check_all(self, current, committed): # Transactions see their own changes. # Read-uncommitted transactions see all changes. # Snapshot and read-committed transactions should not see changes. self.check(self.session, None, current) self.check(self.session2, "isolation=snapshot", committed) self.check(self.session2, "isolation=read-committed", committed) self.check(self.session2, "isolation=read-uncommitted", current) # Opening a clone of the database home directory should run # recovery and see the committed results. self.backup(self.backup_dir) backup_conn_params = 'log=(enabled,file_max=%s)' % self.logmax backup_conn = wiredtiger_open(self.backup_dir, backup_conn_params) try: self.check(backup_conn.open_session(), None, committed) finally: backup_conn.close() def check_log(self, committed): self.backup(self.backup_dir) # # Open and close the backup connection a few times to force # repeated recovery and log archiving even if later recoveries # are essentially no-ops. Confirm that the backup contains # the committed operations after recovery. # # Cycle through the different archive values in a # deterministic manner. self.archive = self.archive_list[self.scenario_number % len(self.archive_list)] backup_conn_params = \ 'log=(enabled,file_max=%s,archive=%s)' % (self.logmax, self.archive) orig_logs = fnmatch.filter(os.listdir(self.backup_dir), "*Log*") endcount = 2 count = 0 while count < endcount: backup_conn = wiredtiger_open(self.backup_dir, backup_conn_params) try: self.check(backup_conn.open_session(), None, committed) finally: # Sleep long enough so that the archive thread is guaranteed # to run before we close the connection. time.sleep(1.0) backup_conn.close() count += 1 # # Check logs after repeated openings. The first log should # have been archived if configured. Subsequent openings would not # archive because no checkpoint is written due to no modifications. # cur_logs = fnmatch.filter(os.listdir(self.backup_dir), "*Log*") for o in orig_logs: if self.archive == 'true': self.assertEqual(False, o in cur_logs) else: self.assertEqual(True, o in cur_logs) # # Run printlog and make sure it exits with zero status. # Printlog should not run recovery nor advance the logs. Make sure # it does not. # self.runWt(['-h', self.backup_dir, 'printlog'], outfilename='printlog.out') pr_logs = fnmatch.filter(os.listdir(self.backup_dir), "*Log*") self.assertEqual(cur_logs, pr_logs) def test_ops(self): # print "Creating %s with config '%s'" % (self.uri, self.create_params) self.session.create(self.uri, self.create_params) # Set up the table with entries for 1, 2, 10 and 11. # We use the overwrite config so insert can update as needed. c = self.session.open_cursor(self.uri, None, 'overwrite') c[1] = c[2] = c[10] = c[11] = 1 current = {1: 1, 2: 1, 10: 1, 11: 1} committed = current.copy() reopen = self.conn_list[self.scenario_number % len(self.conn_list)] ops = (self.op1, self.op2, self.op3, self.op4) txns = (self.txn1, self.txn2, self.txn3, self.txn4) # for ok, txn in zip(ops, txns): # print ', '.join('%s(%d)[%s]' % (ok[0], ok[1], txn) for i, ot in enumerate(zip(ops, txns)): ok, txn = ot op, k = ok # Close and reopen the connection and cursor. if reopen == 'reopen': self.reopen_conn() c = self.session.open_cursor(self.uri, None, 'overwrite') self.session.begin_transaction( (self.scenario_number % 2) and 'sync' or None) # Test multiple operations per transaction by always # doing the same operation on key k + 1. k1 = k + 1 # print '%d: %s(%d)[%s]' % (i, ok[0], ok[1], txn) if op == 'insert' or op == 'update': c[k] = c[k1] = i + 2 current[k] = current[k1] = i + 2 elif op == 'remove': c.set_key(k) c.remove() c.set_key(k1) c.remove() if k in current: del current[k] if k1 in current: del current[k1] # print current # Check the state after each operation. self.check_all(current, committed) if txn == 'commit': committed = current.copy() self.session.commit_transaction() elif txn == 'rollback': current = committed.copy() self.session.rollback_transaction() # Check the state after each commit/rollback. self.check_all(current, committed) # check_log() is slow, we don't run it on every scenario. if self.scenario_number % test_txn02.checklog_mod == 0: self.check_log(committed)
def test_timestamp_randomizer(self): # Local function to generate a random timestamp, or return -1 def maybe_ts(do_gen, iternum): if do_gen: return self.gen_ts(iternum) else: return -1 if wttest.islongtest(): iterations = 100000 else: iterations = 1000 create_params = 'key_format={},value_format={}'.format( self.key_format, self.value_format) self.session.create(self.uri, create_params) self.set_global_timestamps(1, 1, -1) # Create tables with no entries ds = SimpleDataSet(self, self.uri, 0, key_format=self.key_format, value_format=self.value_format) # We do a bunch of iterations, doing transactions, prepare, and global timestamp calls # with timestamps that are sometimes valid, sometimes not. We use the iteration number # as an "approximate timestamp", and generate timestamps for our calls that are near # that number (within 10). Thus, as the test runs, the timestamps generally get larger. # We always know the state of global timestamps, so we can predict the success/failure # on each call. self.commit_value = '<NOT_SET>' for iternum in range(1, iterations): self.pr('\n===== ITERATION ' + str(iternum) + '/' + str(iterations)) self.pr('RANDOM: ({0},{1})'.format(self.rand.seedw, self.rand.seedz)) if self.rand.rand32() % 10 != 0: commit_ts = self.gen_ts(iternum) durable_ts = self.gen_ts(iternum) do_prepare = (self.rand.rand32() % 20 == 0) if self.rand.rand32() % 2 == 0: read_ts = self.gen_ts(iternum) else: read_ts = -1 # no read_timestamp used in txn # OOD does not work with prepared updates. Hence, the commit ts should always be # greater than the last durable ts. if commit_ts <= self.last_durable: commit_ts = self.last_durable + 1 if do_prepare: # If we doing a prepare, we must abide by some additional rules. # If we don't we'll immediately panic if commit_ts < self.oldest_ts: commit_ts = self.oldest_ts if durable_ts < commit_ts: durable_ts = commit_ts if durable_ts <= self.stable_ts: durable_ts = self.stable_ts + 1 value = self.gen_value(iternum, commit_ts) self.updates(value, ds, do_prepare, commit_ts, durable_ts, read_ts) if self.rand.rand32() % 2 == 0: # Set some combination of the global timestamps r = self.rand.rand32() % 16 oldest = maybe_ts((r & 0x1) != 0, iternum) stable = maybe_ts((r & 0x2) != 0, iternum) commit = maybe_ts((r & 0x4) != 0, iternum) durable = maybe_ts((r & 0x8) != 0, iternum) self.set_global_timestamps(oldest, stable, durable) # Make sure the resulting rows are what we expect. cursor = self.session.open_cursor(self.uri) expect_key = 1 expect_value = self.commit_value for k, v in cursor: self.assertEquals(k, expect_key) self.assertEquals(v, expect_value) expect_key += 1 # Although it's theoretically possible to never successfully update a single row, # with a large number of iterations that should never happen. I'd rather catch # a test code error where we mistakenly don't update any rows. self.assertGreater(expect_key, 1) cursor.close()
class test_cursor_bound_fuzz(wttest.WiredTigerTestCase): file_name = 'test_fuzz.wt' iteration_count = 200 if wttest.islongtest() else 50 # For each iteration we do search_count searches that way we test more cases without having to # generate as many key ranges. search_count = 20 key_count = 10000 if wttest.islongtest() else 1000 # Large transactions throw rollback errors so we don't use them in the long test. transactions_enabled = False if wttest.islongtest() else True value_size = 100000 if wttest.islongtest() else 100 prepare_frequency = 5/100 update_frequency = 2/10 min_key = 1 # Max_key is not inclusive so the actual max_key is max_key - 1. max_key = min_key + key_count # A lot of time was spent generating values, to achieve some amount of randomness we pre # generate N values and keep them in memory. value_array = [] value_array_size = 20 current_ts = 1 applied_ops = False key_range = {} types = [ ('file', dict(uri='file:')), ('table', dict(uri='table:')) ] data_format = [ ('row', dict(key_format='i')), ('column', dict(key_format='r')) ] scenarios = make_scenarios(types, data_format) # Iterates valid keys from min_key to max_key, the maximum key is defined as max_key - 1. # Python doesn't consider the end of the range as inclusive. def key_range_iter(self): for i in range(self.min_key, self.max_key): yield i def dump_key_range(self): for i in self.key_range_iter(): self.pr(self.key_range[i].to_string()) # Generate a random ascii value. def generate_value(self): return ''.join(random.choice(string.ascii_lowercase) for _ in range(self.value_size)) # Get a value from the value array. def get_value(self): return self.value_array[random.randrange(self.value_array_size)] # Get a key within the range of min_key and max_key. def get_random_key(self): return random.randrange(self.min_key, self.max_key) # Update a key using the cursor and update its in memory representation. def apply_update(self, cursor, key_id, prepare): value = self.get_value() cursor[key_id] = value self.key_range[key_id].update(value, key_states.UPSERTED, self.current_ts, prepare) self.verbose(3, "Updating " + self.key_range[key_id].to_string()) # Remove a key using the cursor and mark it as deleted in memory. # If the key is already deleted we skip the remove. def apply_remove(self, cursor, key_id, prepare): if (self.key_range[key_id].is_deleted()): return cursor.set_key(key_id) self.assertEqual(cursor.remove(), 0) self.key_range[key_id].update(None, key_states.DELETED, self.current_ts, prepare) self.verbose(3, "Removing " + self.key_range[key_id].to_string()) # Apply a truncate operation to the key range. def apply_truncate(self, session, cursor, cursor2, prepare): lower_key = self.get_random_key() if (lower_key + 1 < self.max_key): upper_key = random.randrange(lower_key + 1, self.max_key) cursor.set_key(lower_key) cursor2.set_key(upper_key) self.assertEqual(session.truncate(None, cursor, cursor2, None), 0) # Mark all keys from lower_key to upper_key as deleted. for key_id in range(lower_key, upper_key + 1): self.key_range[key_id].update(None, key_states.DELETED, self.current_ts, prepare) self.verbose(2, "Truncated keys between: " + str(lower_key) + " and: " + str(upper_key)) # Each iteration calls this function once to update the state of the keys in the database and # in memory. def apply_ops(self, session, cursor, prepare): op = random.choice(list(operations)) if (op is operations.TRUNCATE and self.applied_ops): cursor2 = session.open_cursor(self.uri + self.file_name) self.apply_truncate(session, cursor, cursor2, prepare) else: for i in self.key_range_iter(): if (random.uniform(0, 1) < self.update_frequency): continue op = random.choice(list(operations)) if (op is operations.TRUNCATE): pass elif (op is operations.UPSERT): self.apply_update(cursor, i, prepare) elif (op is operations.REMOVE): self.apply_remove(cursor, i, prepare) else: raise Exception("Unhandled operation generated") self.applied_ops = True # As prepare throws a prepare conflict exception we wrap the call to anything that could # encounter a prepare conflict in a try except, we then return the error code to the caller. def prepare_call(self, func): try: ret = func() except wiredtiger.WiredTigerError as e: if wiredtiger.wiredtiger_strerror(wiredtiger.WT_PREPARE_CONFLICT) in str(e): ret = wiredtiger.WT_PREPARE_CONFLICT else: raise e return ret # Once we commit the prepared transaction, update and clear the prepared flags. def clear_prepare_key_ranges(self): for i in self.key_range_iter(): self.key_range[i].clear_prepared() # Given a bound, this functions returns the start or end expected key of the bounded range. # Note the type argument determines if we return the start or end limit. e.g. if we have a lower # bound then the key would be the lower bound, however if the lower bound isn't enabled then the # lowest possible key would be min_key. max_key isn't inclusive so we subtract 1 off it. def get_expected_limit_key(self, bound_set, type): if (type == bound_type.LOWER): if (bound_set.lower.enabled): if (bound_set.lower.inclusive): return bound_set.lower.key return bound_set.lower.key + 1 return self.min_key if (bound_set.upper.enabled): if (bound_set.upper.inclusive): return bound_set.upper.key return bound_set.upper.key - 1 return self.max_key - 1 # When a prepared cursor walks next or prev it can skip deleted records internally before # returning a prepare conflict, we don't know which key it got to so we need to validate that # we see a series of deleted keys followed by a prepared key. def validate_deleted_prepared_range(self, start_key, end_key, next): if (next): step = 1 else: step = -1 self.verbose(2, "Walking deleted range from: " + str(start_key) + " to: " + str(end_key)) for i in range(start_key, end_key, step): self.verbose(3, "Validating state of key: " + self.key_range[i].to_string()) if (self.key_range[i].is_prepared()): return elif (self.key_range[i].is_deleted()): continue else: self.assertTrue(False) # Validate a prepare conflict in the cursor->next scenario. def validate_prepare_conflict_next(self, current_key, bound_set): self.verbose(3, "Current key is: " + str(current_key) + " min_key is: " + str(self.min_key)) start_range = None if current_key == self.min_key: # We hit a prepare conflict while walking forwards before we stepped to a valid key. # Therefore validate all the keys from start of the range are deleted followed by a prepare. start_range = self.get_expected_limit_key(bound_set, bound_type.LOWER) else: # We walked part of the way through a valid key range before we hit the prepared # update. Therefore validate the range between our current key and the # end range. start_range = current_key end_range = self.get_expected_limit_key(bound_set, bound_type.UPPER) # Perform validation from the start range to end range. self.validate_deleted_prepared_range(start_range, end_range, True) # Validate a prepare conflict in the cursor->prev scenario. def validate_prepare_conflict_prev(self, current_key, bound_set): self.verbose(3, "Current key is: " + str(current_key) + " max_key is: " + str(self.max_key)) start_range = None if current_key == self.max_key - 1: # We hit a prepare conflict while walking backwards before we stepped to a valid key. # Therefore validate all the keys from start of the range are deleted followed by a # prepare. start_range = self.get_expected_limit_key(bound_set, bound_type.UPPER) else: # We walked part of the way through a valid key range before we hit the prepared # update. Therefore validate the range between our current key and the # end range. start_range = current_key end_range = self.get_expected_limit_key(bound_set, bound_type.LOWER) # Perform validation from the start range to end range. self.validate_deleted_prepared_range(start_range, end_range, False) # Walk the cursor using cursor->next and validate the returned keys. def run_next(self, bound_set, cursor): # This array gives us confidence that we have validated the full key range. checked_keys = [] self.verbose(2, "Running scenario: NEXT") key_range_it = self.min_key - 1 ret = self.prepare_call(lambda: cursor.next()) while (ret != wiredtiger.WT_NOTFOUND and ret != wiredtiger.WT_PREPARE_CONFLICT): current_key = cursor.get_key() current_value = cursor.get_value() self.verbose(3, "Cursor next walked to key: " + str(current_key) + " value: " + current_value) self.assertTrue(bound_set.in_bounds_key(current_key)) self.assertTrue(self.key_range[current_key].equals(current_key, current_value)) checked_keys.append(current_key) # If the cursor has walked to a record that isn't +1 our current record then it # skipped something internally. # Check that the key range between key_range_it and current_key isn't visible if (current_key != key_range_it + 1): for i in range(key_range_it + 1, current_key): self.verbose(3, "Checking key is deleted or oob: " + str(i)) checked_keys.append(i) self.assertTrue(self.key_range[i].is_deleted_or_oob(bound_set)) key_range_it = current_key ret = self.prepare_call(lambda: cursor.next()) key_range_it = key_range_it + 1 # If we were returned a prepare conflict it means the cursor has found a prepared key/value. # We need to validate that it arrived there correctly using the in memory state of the # database. We cannot continue from a prepare conflict so we return. if (ret == wiredtiger.WT_PREPARE_CONFLICT): self.validate_prepare_conflict_next(key_range_it, bound_set) return # If key_range_it is < key_count then the rest of the range was deleted # Remember to increment it by one to get it to the first not in bounds key. for i in range(key_range_it, self.max_key): checked_keys.append(i) self.verbose(3, "Checking key is deleted or oob: " + str(i)) self.assertTrue(self.key_range[i].is_deleted_or_oob(bound_set)) self.assertTrue(len(checked_keys) == self.key_count) # Walk the cursor using cursor->prev and validate the returned keys. def run_prev(self, bound_set, cursor): # This array gives us confidence that we have validated the full key range. checked_keys = [] self.verbose(2, "Running scenario: PREV") ret = self.prepare_call(lambda: cursor.prev()) key_range_it = self.max_key while (ret != wiredtiger.WT_NOTFOUND and ret != wiredtiger.WT_PREPARE_CONFLICT): current_key = cursor.get_key() current_value = cursor.get_value() self.verbose(3, "Cursor prev walked to key: " + str(current_key) + " value: " + current_value) self.assertTrue(bound_set.in_bounds_key(current_key)) self.assertTrue(self.key_range[current_key].equals(current_key, current_value)) checked_keys.append(current_key) # If the cursor has walked to a record that isn't -1 our current record then it # skipped something internally. # Check that the key range between key_range_it and current_key isn't visible if (current_key != key_range_it - 1): # Check that the key range between key_range_it and current_key isn't visible for i in range(current_key + 1, key_range_it): self.verbose(3, "Checking key is deleted or oob: " + str(i)) checked_keys.append(i) self.assertTrue(self.key_range[i].is_deleted_or_oob(bound_set)) key_range_it = current_key ret = self.prepare_call(lambda: cursor.prev()) # If key_range_it is > key_count then the rest of the range was deleted key_range_it -= 1 if (ret == wiredtiger.WT_PREPARE_CONFLICT): self.validate_prepare_conflict_prev(key_range_it, bound_set) return for i in range(self.min_key, key_range_it + 1): checked_keys.append(i) self.verbose(3, "Checking key is deleted or oob: " + str(i)) self.assertTrue(self.key_range[i].is_deleted_or_oob(bound_set)) self.assertTrue(len(checked_keys) == self.key_count) # Run basic cursor->search() scenarios and validate the outcome. def run_search(self, bound_set, cursor): # Choose a N random keys and perform a search on each for i in range(0, self.search_count): search_key = self.get_random_key() cursor.set_key(search_key) ret = self.prepare_call(lambda: cursor.search()) if (ret == wiredtiger.WT_PREPARE_CONFLICT): self.assertTrue(self.key_range[search_key].is_prepared()) elif (ret == wiredtiger.WT_NOTFOUND): self.assertTrue(self.key_range[search_key].is_deleted_or_oob(bound_set)) elif (ret == 0): # Assert that the key exists, and is within the range. self.assertTrue(self.key_range[search_key].equals(cursor.get_key(), cursor.get_value())) self.assertTrue(bound_set.in_bounds_key(cursor.get_key())) else: raise Exception('Unhandled error returned by search') # Check that all the keys within the given bound_set are deleted. def check_all_within_bounds_not_visible(self, bound_set): for i in range(bound_set.start_range(self.min_key), bound_set.end_range(self.max_key)): self.verbose(3, "checking key: " +self.key_range[i].to_string()) if (not self.key_range[i].is_deleted()): return False return True # Run a cursor->search_near scenario and validate that the outcome was correct. def run_search_near(self, bound_set, cursor): # Choose N random keys and perform a search near. for i in range(0, self.search_count): search_key = self.get_random_key() cursor.set_key(search_key) self.verbose(2, "Searching for key: " + str(search_key)) ret = self.prepare_call(lambda: cursor.search_near()) if (ret == wiredtiger.WT_NOTFOUND): self.verbose(2, "Nothing visible checking.") # Nothing visible within the bound range. # Validate. elif (ret == wiredtiger.WT_PREPARE_CONFLICT): # Due to the complexity of the search near logic we will simply check if there is # a prepared key within the range. found_prepare = False for i in range(bound_set.start_range(self.min_key), bound_set.end_range(self.max_key)): if (self.key_range[i].is_prepared()): found_prepare = True break self.assertTrue(found_prepare) self.verbose(2, "Received prepare conflict in search near.") else: key_found = cursor.get_key() self.verbose(2, "Found a key: " + str(key_found)) current_key = key_found # Assert the value we found matches. # Equals also validates that the key is visible. self.assertTrue(self.key_range[current_key].equals(current_key, cursor.get_value())) if (bound_set.in_bounds_key(search_key)): # We returned a key within the range, validate that key is the one that # should've been returned. if (key_found == search_key): # We've already deteremined the key matches. We can return. pass if (key_found > search_key): # Walk left and validate that all isn't visible to the search key. while (current_key != search_key): current_key = current_key - 1 self.assertTrue(self.key_range[current_key].is_deleted()) if (key_found < search_key): # Walk right and validate that all isn't visible to the search key. while (current_key != search_key): current_key = current_key + 1 self.assertTrue(self.key_range[current_key].is_deleted()) else: # We searched for a value outside our range, we should return whichever value # is closest within the range. if (bound_set.lower.enabled and search_key <= bound_set.lower.key): # We searched to the left of our bounds. In the equals case the lower bound # must not be inclusive. # Validate that the we returned the nearest value to the lower bound. if (bound_set.lower.inclusive): self.assertTrue(key_found >= bound_set.lower.key) current_key = bound_set.lower.key else: self.assertTrue(key_found > bound_set.lower.key) current_key = bound_set.lower.key + 1 while (current_key != key_found): self.assertTrue(self.key_range[current_key].is_deleted()) current_key = current_key + 1 elif (bound_set.upper.enabled and search_key >= bound_set.upper.key): # We searched to the right of our bounds. In the equals case the upper bound # must not be inclusive. # Validate that the we returned the nearest value to the upper bound. if (bound_set.upper.inclusive): self.assertTrue(key_found <= bound_set.upper.key) current_key = bound_set.upper.key else: self.assertTrue(key_found < bound_set.upper.key) current_key = bound_set.upper.key - 1 while (current_key != key_found): self.assertTrue(self.key_range[current_key].is_deleted()) current_key = current_key - 1 else: raise Exception('Illegal state found in search_near') # Choose a scenario and run it. def run_bound_scenarios(self, bound_set, cursor): scenario = random.choice(list(bound_scenarios)) if (scenario is bound_scenarios.NEXT): self.run_next(bound_set, cursor) elif (scenario is bound_scenarios.PREV): self.run_prev(bound_set, cursor) elif (scenario is bound_scenarios.SEARCH): self.run_search(bound_set, cursor) elif (scenario is bound_scenarios.SEARCH_NEAR): self.run_search_near(bound_set, cursor) else: raise Exception('Unhandled bound scenario chosen') # Generate a set of bounds and apply them to the cursor. def apply_bounds(self, cursor): cursor.reset() lower = bound(self.get_random_key(), bool(random.getrandbits(1)), bool(random.getrandbits(1))) upper = bound(random.randrange(lower.key, self.max_key), bool(random.getrandbits(1)), bool(random.getrandbits(1))) # Prevent invalid bounds being generated. if (lower.key == upper.key and lower.enabled and upper.enabled): lower.inclusive = upper.inclusive = True bound_set = bounds(lower, upper) if (lower.enabled): cursor.set_key(lower.key) cursor.bound("bound=lower,inclusive=" + lower.inclusive_str()) if (upper.enabled): cursor.set_key(upper.key) cursor.bound("bound=upper,inclusive=" + upper.inclusive_str()) return bound_set # The primary test loop is contained here. def test_bound_fuzz(self): uri = self.uri + self.file_name create_params = 'value_format=S,key_format={}'.format(self.key_format) # Reset the key range for every scenario. self.key_range = {} # Setup a reproducible random seed. # If this test fails inspect the file WT_TEST/results.txt and replace the time.time() # with a given seed. e.g.: # seed = 1660215872.5926154 # Additionally this test is configured for verbose logging which can make debugging a bit # easier. seed = time.time() self.pr("Using seed: " + str(seed)) random.seed(seed) self.session.create(uri, create_params) read_cursor = self.session.open_cursor(uri) write_session = self.setUpSessionOpen(self.conn) write_cursor = write_session.open_cursor(uri) # Initialize the value array. self.verbose(2, "Generating value array") for i in range(0, self.value_array_size): self.value_array.append(self.generate_value()) # Initialize the key range. for i in self.key_range_iter(): key_value = self.get_value() self.key_range[i] = key(i, key_value, key_states.UPSERTED, self.current_ts) self.current_ts += 1 if (self.transactions_enabled): write_session.begin_transaction() write_cursor[i] = key_value if (self.transactions_enabled): write_session.commit_transaction('commit_timestamp=' + self.timestamp_str(self.key_range[i].timestamp)) self.session.checkpoint() # Begin main loop for i in range(0, self.iteration_count): self.verbose(2, "Iteration: " + str(i)) bound_set = self.apply_bounds(read_cursor) self.verbose(2, "Generated bound set: " + bound_set.to_string()) # Check if we are doing a prepared transaction on this iteration. prepare = random.uniform(0, 1) <= self.prepare_frequency and self.transactions_enabled if (self.transactions_enabled): write_session.begin_transaction() self.apply_ops(write_session, write_cursor, prepare) if (self.transactions_enabled): if (prepare): self.verbose(2, "Preparing applied operations.") write_session.prepare_transaction('prepare_timestamp=' + self.timestamp_str(self.current_ts)) else: write_session.commit_transaction('commit_timestamp=' + self.timestamp_str(self.current_ts)) # Use the current timestamp so we don't need to track previous versions. if (self.transactions_enabled): self.session.begin_transaction('read_timestamp=' + self.timestamp_str(self.current_ts)) self.run_bound_scenarios(bound_set, read_cursor) if (self.transactions_enabled): self.session.rollback_transaction() if (prepare): write_session.commit_transaction( 'commit_timestamp=' + self.timestamp_str(self.current_ts) + ',durable_timestamp='+ self.timestamp_str(self.current_ts)) self.clear_prepare_key_ranges() self.current_ts += 1 if (i % 10 == 0): # Technically this is a write but easier to do it with this session. self.session.checkpoint()
class test_backup26(backup_base): dir = 'backup.dir' # Backup directory name uri = "table_backup" ntables = 10000 if wttest.islongtest() else 500 # Reverse the backup restore list, WiredTiger should still succeed in this case. reverse = [ ["reverse_target_list", dict(reverse=True)], ["target_list", dict(reverse=False)], ] # Percentage of tables to not copy over in selective backup. percentage = [ ('hundred_precent', dict(percentage=1)), ('ninety_percent', dict(percentage=0.9)), ('fifty_percent', dict(percentage=0.5)), ('ten_percent', dict(percentage=0.1)), ('zero_percent', dict(percentage=0)), ] scenarios = make_scenarios(percentage, reverse) def test_backup26(self): selective_remove_uri_file_list = [] selective_remove_uri_list = [] selective_uri_list = [] for i in range(0, self.ntables): uri = "table:{0}".format(self.uri + str(i)) dataset = SimpleDataSet(self, uri, 100, key_format="S") dataset.populate() # Append the table uri to the selective backup remove list until the set percentage. # These tables will not be copied over in selective backup. if (i <= int(self.ntables * self.percentage)): selective_remove_uri_list.append(uri) selective_remove_uri_file_list.append( "{0}.wt".format(self.uri + str(i))) else: selective_uri_list.append(uri) self.session.checkpoint() os.mkdir(self.dir) # Now copy the files using full backup. This should not include the tables inside the remove list. all_files = self.take_selective_backup(self.dir, selective_remove_uri_file_list) target_uris = None if self.reverse: target_uris = str(selective_uri_list[::-1]).replace("\'", "\"") else: target_uris = str(selective_uri_list).replace("\'", "\"") starttime = time.time() # After the full backup, open and recover the backup database. backup_conn = self.wiredtiger_open( self.dir, "backup_restore_target={0}".format(target_uris)) elapsed = time.time() - starttime self.pr("%s partial backup has taken %.2f seconds." % (str(self), elapsed)) bkup_session = backup_conn.open_session() # Open the cursor from uris that were not part of the selective backup and expect failure # since file doesn't exist. for remove_uri in selective_remove_uri_list: self.assertRaisesException( wiredtiger.WiredTigerError, lambda: bkup_session.open_cursor(remove_uri, None, None)) # Open the cursors on tables that copied over to the backup directory. They should still # recover properly. for uri in selective_uri_list: c = bkup_session.open_cursor(uri, None, None) ds = SimpleDataSet(self, uri, 100, key_format="S") ds.check_cursor(c) c.close() backup_conn.close()