Skip to content

Commit

Permalink
Make zapping MySQL faster.
Browse files Browse the repository at this point in the history
Using DROP TABLE statements instead of DELETE. This isn't fully transactional prior to MySQL 8.0, but that shouldn't matter.

Fixes #242
  • Loading branch information
jamadden committed Jun 20, 2019
1 parent 16bbbb3 commit 7ca382d
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 34 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Expand Up @@ -8,6 +8,10 @@
- Zapping a storage now also removes any persistent cache files. See
:issue:`241`.

- Zapping a MySQL storage now issues ``DROP TABLE`` statements instead
of ``DELETE FROM`` statements. This is much faster on large
databases. See :issue:`242`.

3.0a2 (2019-06-19)
==================

Expand Down
26 changes: 16 additions & 10 deletions src/relstorage/adapters/mysql/schema.py
Expand Up @@ -22,6 +22,7 @@
from ..interfaces import ISchemaInstaller
from ..schema import AbstractSchemaInstaller

logger = __import__('logging').getLogger(__name__)

@implementer(ISchemaInstaller)
class MySQLSchemaInstaller(AbstractSchemaInstaller):
Expand Down Expand Up @@ -62,9 +63,9 @@ def _create_pack_lock(self, cursor):
COLTYPE_BINARY_STRING = 'BLOB'
TRANSACTIONAL_TABLE_SUFFIX = 'ENGINE = InnoDB'

# As usual, MySQL has an annoying implementation of this and we
# As usual, MySQL has a quirky implementation of this feature and we
# have to re-specify *everything* about the column. MySQL 8 supports the
# simple 'RENAME ... TO ... syntax that everyone else does.
# simple 'RENAME ... TO ...' syntax that everyone else does.
_rename_transaction_empty_stmt = (
"ALTER TABLE transaction CHANGE empty is_empty "
"BOOLEAN NOT NULL DEFAULT FALSE"
Expand Down Expand Up @@ -239,14 +240,19 @@ def _create_temp_pack_visit(self, _cursor):
def _create_temp_undo(self, _cursor):
return

def _init_after_create(self, cursor):
if self.keep_history:
stmt = """
INSERT INTO transaction (tid, username, description)
VALUES (0, 'system', 'special transaction for object creation');
"""
self.runner.run_script(cursor, stmt)

def _reset_oid(self, cursor):
stmt = "TRUNCATE new_oid;"
self.runner.run_script(cursor, stmt)

# We can't TRUNCATE tables that have foreign-key relationships
# with other tables, but we can drop them. This has to be followed up by
# creating them again.
_zap_all_tbl_stmt = 'DROP TABLE %s'

def _after_zap_all_tables(self, cursor, slow=False):
if not slow:
logger.debug("Creating tables after drop")
self.create(cursor)
logger.debug("Done creating tables after drop")
else:
super(MySQLSchemaInstaller, self)._after_zap_all_tables(cursor, slow)
8 changes: 6 additions & 2 deletions src/relstorage/adapters/postgresql/adapter.py
Expand Up @@ -210,15 +210,19 @@ def new_instance(self):
return inst

def __str__(self):
parts = [self.__class__.__name__]
parts = []
if self.keep_history:
parts.append('history preserving')
else:
parts.append('history free')
dsnparts = self._dsn.split()
s = ' '.join(p for p in dsnparts if not p.startswith('password'))
parts.append('dsn=%r' % s)
return ", ".join(parts)
return "<%s at %x %s>" % (
self.__class__.__name__, id(self), ",".join(parts)
)

__repr__ = __str__



Expand Down
22 changes: 14 additions & 8 deletions src/relstorage/adapters/schema.py
Expand Up @@ -380,7 +380,8 @@ def _needs_transaction_empty_update(self, cursor):

_rename_transaction_empty_stmt = 'ALTER TABLE transaction RENAME COLUMN empty TO is_empty'

_zap_all_tbl_stmt = 'DELETE FROM %s'
# Subclasses can redefine these.
_slow_zap_all_tbl_stmt = _zap_all_tbl_stmt = 'DELETE FROM %s'

def zap_all(self, reset_oid=True, slow=False):
"""
Expand All @@ -391,24 +392,24 @@ def zap_all(self, reset_oid=True, slow=False):
DELETEd. This is helpful when other connections might be open and
holding some kind of locks.
"""
stmt = self._zap_all_tbl_stmt if not slow else AbstractSchemaInstaller._zap_all_tbl_stmt
stmt = self._zap_all_tbl_stmt if not slow else self._slow_zap_all_tbl_stmt

def callback(_conn, cursor):
existent = set(self.list_tables(cursor))
todo = reversed(self.all_tables)
todo = list(self.all_tables)
todo.reverse() # using reversed() doesn't print nicely
log.debug("Checking tables: %r", todo)
for table in todo:
log.debug("Considering table %s", table)
if table.startswith('temp_'):
continue
if table in existent:
log.debug("Deleting from table %s...", table)
cursor.execute(stmt % table)
table_stmt = stmt % table
log.debug(table_stmt)
cursor.execute(table_stmt)
log.debug("Done deleting from tables.")

log.debug("Running init script.")
self._init_after_create(cursor)
log.debug("Done running init script.")
self._after_zap_all_tables(cursor, slow)

if reset_oid:
log.debug("Running OID reset script.")
Expand All @@ -417,6 +418,11 @@ def callback(_conn, cursor):

self.connmanager.open_and_call(callback)

def _after_zap_all_tables(self, cursor, slow=False):
log.debug("Running init script. Slow: %s", slow)
self._init_after_create(cursor)
log.debug("Done running init script.")


def drop_all(self):
"""Drop all tables and sequences."""
Expand Down
5 changes: 4 additions & 1 deletion src/relstorage/tests/reltestbase.py
Expand Up @@ -122,7 +122,10 @@ def make_storage(self, zap=True, **kw):
# with them? This leads to connections remaining open with
# locks on PyPy, so on PostgreSQL we can't TRUNCATE tables
# and have to go the slow route.
storage.zap_all(slow=True)
#
# As of 2019-06-20 with PyPy 7.1.1, I'm no longer able to replicate
# a problem like that locally, so we go back to the fast way.
storage.zap_all()
return self._wrap_storage(storage)


Expand Down
20 changes: 8 additions & 12 deletions src/relstorage/tests/testpostgresql.py
Expand Up @@ -25,26 +25,22 @@
class PostgreSQLAdapterMixin(object):

def make_adapter(self, options, db=None):
if db is None:
if self.keep_history:
db = self.base_dbname
else:
db = self.base_dbname + '_hf'
return PostgreSQLAdapter(
dsn='dbname=%s user=relstoragetest password=relstoragetest' % db,
dsn=self.__get_adapter_zconfig_dsn(db),
options=options,
)

def get_adapter_class(self):
return PostgreSQLAdapter

def __get_adapter_zconfig_dsn(self):
if self.keep_history:
dbname = self.base_dbname
else:
dbname = self.base_dbname + '_hf'
def __get_adapter_zconfig_dsn(self, dbname=None):
if dbname is None:
if self.keep_history:
dbname = self.base_dbname
else:
dbname = self.base_dbname + '_hf'
dsn = (
"dbname='%s' user='relstoragetest' password='relstoragetest'"
"dbname='%s' user='relstoragetest' host='127.0.0.1' password='relstoragetest'"
% dbname
)
return dsn
Expand Down
3 changes: 2 additions & 1 deletion src/relstorage/tests/util.py
Expand Up @@ -276,8 +276,9 @@ def create_storage(name, blob_dir,

adapter_maker = self.use_adapter()
adapter = adapter_maker.make_adapter(options, db)
__traceback_info__ = adapter, options
storage = RelStorage(adapter, name=name, options=options)
storage.zap_all(slow=True)
storage.zap_all()
return storage

prefix = '%s_%s%s' % (
Expand Down

0 comments on commit 7ca382d

Please sign in to comment.