git » nmdb » master » tree

[master] / tests / python / random1.py

#!/usr/bin/env python

"""
This is a stress test for the nmdb server, library, and Python (2 and 3)
bindings.

It creates a number of keys, and then operates randomly on them, verifying
that everything is working as expected.

It can operate with the database or only with the cache.

You can run it with both Python 2 and 3.
"""

import sys
import nmdb
from random import randint, choice


class Mismatch (Exception):
	"Mismatch between local and remote database"
	pass



# History of each key, global because we use it from several places for
# debugging purposes
history = {}

def checked(f):
	"Prints the history of the key when an operation fails"
	def newf(self, k, *args, **kwargs):
		try:
			return f(self, k, *args, **kwargs)
		except:
			if k in history:
				print(history[k])
			else:
				print('No history for key', k)
			raise
	newf.__name__ = f.__name__
	return newf


class Tester:
	"Common code for tester classes"
	def __init__(self):
		# nmdb connection
		self.ndb = self.connect()
		self.ndb.add_tipc_server()
		self.ndb.add_tcp_server('localhost')
		self.ndb.add_udp_server('localhost')

		# local database
		self.ldb = {}

	def connect(self):
		"Connects to an nmdb instance"
		raise NotImplementedError

	@checked
	def set(self, k, v):
		self.ndb[k] = v
		self.ldb[k] = v
		if k not in history:
			history[k] = []
		history[k].append((self.set, k, v))

	def check(self):
		"Checks all entries in ldb match the ones in ndb"
		n = l = None
		for k in self.ldb.keys():
			# get() verifies they match so we do not need to care
			# about that
			self.get(k)

	def get(self, k):
		raise NotImplementedError

	def delete(self, k):
		raise NotImplementedError

	def cas(self, k, ov, nv):
		raise NotImplementedError

	def find_missing(self):
		raise NotImplementedError


class DBTester (Tester):
	"Tester for db mode"

	def connect(self):
		return nmdb.DB()

	@checked
	def get(self, k):
		n = self.ndb[k]
		l = self.ldb[k]
		if l != n:
			raise Mismatch((n, l))
		history[k].append((self.get, k))
		return n

	@checked
	def delete(self, k):
		del self.ndb[k]
		del self.ldb[k]
		history[k].append((self.delete, k))

	@checked
	def cas(self, k, ov, nv):
		prel = self.ldb[k]
		pren = self.ndb[k]
		n = self.ndb.cas(k, ov, nv)
		if k not in self.ldb:
			l = 0
		elif self.ldb[k] == ov:
			self.ldb[k] = nv
			l = 2
		else:
			l = 1
		if n != l:
			print(k, self.ldb[k], self.ndb[k])
			print(prel, pren)
			print(history[k])
			raise Mismatch((n, l))
		history[k].append((self.cas, k, ov, nv))
		return n

	def find_missing(self):
		# we just check we can get them all
		for k in self.ldb.keys():
			self.get(k)
		return 0


class CacheTester (Tester):
	"Tester for cache mode"

	def connect(self):
		return nmdb.Cache()

	@checked
	def get(self, k):
		try:
			n = self.ndb[k]
		except KeyError:
			del self.ldb[k]
			del history[k]
			return False

		l = self.ldb[k]
		if l != n:
			raise Mismatch((n, l))
		history[k].append((self.get, k))
		return True

	@checked
	def delete(self, k):
		del self.ldb[k]
		try:
			del self.ndb[k]
		except KeyError:
			pass
		history[k].append((self.delete, k))

	def find_missing(self):
		misses = 0
		for k in list(self.ldb.keys()):
			if not self.get(k):
				misses += 1
		return misses


# Use integers because the normal random() generates floating point numbers,
# and they can mess up comparisons because of architecture details.
def getrand():
	return randint(0, 1000000000000000000)


def main():
	# We want to always use a generator range(), which has different names
	# in Python 2 and 3, so isolate the code from this hack
	if sys.version_info[0] == 2:
		gen_range = xrange
	else:
		gen_range = range

	if len(sys.argv) < 3:
		print('Use: random1.py db|cache number_of_keys [key_prefix]')
		sys.exit(1)

	mode = sys.argv[1]
	if mode not in ('db', 'cache'):
		print('Error: mode must be either db or cache')
		sys.exit(1)

	nkeys = int(sys.argv[2])
	if len(sys.argv) >= 4:
		key_prefix = sys.argv[3]
	else:
		key_prefix = ''

	if mode == 'db':
		tester = DBTester()
	else:
		tester = CacheTester()

	# fill all the keys
	print('populate')
	for i in gen_range(nkeys):
		tester.set(key_prefix + str(getrand()), getrand())

	print('missing', tester.find_missing())

	lkeys = list(tester.ldb.keys())

	# operate on them a bit
	print('random operations')

	if mode == 'db':
		operations = ('set', 'delete', 'cas0', 'cas1')
	else:
		operations = ('set', 'get', 'delete')

	for i in gen_range(min(len(lkeys), nkeys // 2)):
		op = choice(operations)
		k = choice(lkeys)
		if op == 'set':
			tester.set(k, getrand())
		elif op == 'get':
			tester.get(k)
		elif op == 'delete':
			tester.delete(k)
			lkeys.remove(k)
		elif op == 'cas0':
			# unsucessful cas
			tester.cas(k, -1, -1)
		elif op == 'cas1':
			# successful cas
			tester.cas(k, tester.ldb[k], getrand())

	print('missing', tester.find_missing())

	print('check')
	tester.check()

	print('delete')
	for k in lkeys:
		tester.delete(k)

	print('check')
	tester.check()


if __name__ == '__main__':
	main()