1
0
mirror of https://github.com/quay/quay.git synced 2026-01-26 06:21:37 +03:00
Files
quay/data/readreplica.py
Joseph Schorr 0844256a71 Major post-OCI cleanup (#225)
* Remove V3_UPGRADE_MODE

* Remove tag backfill worker and all callers to tag backfill and upgrades

* Change initdb to create all test data via the manifest builder, rather than manually via legacy images

* Convert various code paths to use the registry_model where we previously did not do so

* Convert the GC test suite to build via the manifest builder

* Delete an old, unused tool

* Delete the Pre OCI registry model

* Add additional error handling to the manifest creation code path

* Add additional error handling to the OCI tag creation code path

* Change how we respond to invalid manifest content types to better handle unknowns

* Change legacy secscan test suite to use the registry model

* Change the repo build badge to use the registry model (also fixes a bug)

* Delete now-unused data model code

* Remove old model adjustment code from OCI model

* Mark older data models as deprecated which will prevent new rows from being inserted

* Remove references to old registry test suite from various testing files

* Remove tag backfill worker (again; got re-added during rebase)

* Move all deprecated model checks into a central function

* Make data_migration more Pythonic

* Small requested fixes to Tag module styling

* Have tag backfill worker fail to migrate if there are TagManifest's

Since this backfill should only be called in future releases for empty models, this should catch someone attempting to upgrade from a too-old version

* Remove labelbackfillworker as it is no longer needed

* Remove unused invalid import

* Reimplement the tag test for the remaining method used
2020-03-17 13:27:29 -04:00

147 lines
5.1 KiB
Python

import random
from collections import namedtuple
from peewee import Model, SENTINEL, OperationalError, Proxy
from data.decorators import is_deprecated_model
ReadOnlyConfig = namedtuple("ReadOnlyConfig", ["is_readonly", "read_replicas"])
class ReadOnlyModeException(Exception):
"""
Exception raised if a write operation was attempted when in read only mode.
"""
class AutomaticFailoverWrapper(object):
"""
Class which wraps a peewee database driver and (optionally) a second driver.
When executing SQL, if an OperationalError occurs, if a second driver is given, the query is
attempted again on the fallback DB. Otherwise, the exception is raised.
"""
def __init__(self, primary_db, fallback_db=None):
self._primary_db = primary_db
self._fallback_db = fallback_db
def __getattr__(self, attribute):
if attribute != "execute_sql" and hasattr(self._primary_db, attribute):
return getattr(self._primary_db, attribute)
return getattr(self, attribute)
def execute(self, query, commit=SENTINEL, **context_options):
ctx = self.get_sql_context(**context_options)
sql, params = ctx.sql(query).query()
return self.execute_sql(sql, params, commit=commit)
def execute_sql(self, sql, params=None, commit=SENTINEL):
try:
return self._primary_db.execute_sql(sql, params, commit)
except OperationalError:
if self._fallback_db is not None:
try:
return self._fallback_db.execute_sql(sql, params, commit)
except OperationalError:
raise
class ReadReplicaSupportedModel(Model):
"""
Base model for peewee data models that support using a read replica for SELECT requests not
under transactions, and automatic failover to the master if the read replica fails.
Read-only queries are initially attempted on one of the read replica databases
being used; if an OperationalError occurs when attempting to invoke the query,
then the failure is logged and the query is retried on the database master.
Queries that are non-SELECTs (or under transactions) are always tried on the
master.
If the system is configured into read only mode, then all non-read-only queries
will raise a ReadOnlyModeException.
"""
@classmethod
def _read_only_config(cls):
read_only_config = getattr(cls._meta, "read_only_config", None)
if read_only_config is None:
return ReadOnlyConfig(False, [])
if isinstance(read_only_config, Proxy) and read_only_config.obj is None:
return ReadOnlyConfig(False, [])
return read_only_config.obj or ReadOnlyConfig(False, [])
@classmethod
def _in_readonly_mode(cls):
return cls._read_only_config().is_readonly
@classmethod
def _select_database(cls):
"""
Selects a read replica database if we're configured to support read replicas.
Otherwise, selects the master database.
"""
# Select the master DB if read replica support is not enabled.
read_only_config = cls._read_only_config()
if not read_only_config.read_replicas:
return cls._meta.database
# Select the master DB if we're ever under a transaction.
if cls._meta.database.transaction_depth() > 0:
return cls._meta.database
# Otherwise, return a read replica database with auto-retry onto the main database.
replicas = read_only_config.read_replicas
selected_read_replica = replicas[random.randrange(len(replicas))]
return AutomaticFailoverWrapper(selected_read_replica, cls._meta.database)
@classmethod
def select(cls, *args, **kwargs):
query = super(ReadReplicaSupportedModel, cls).select(*args, **kwargs)
query._database = cls._select_database()
return query
@classmethod
def insert(cls, *args, **kwargs):
if is_deprecated_model(cls):
raise Exception("Attempt to write to deprecated model %s" % cls)
query = super(ReadReplicaSupportedModel, cls).insert(*args, **kwargs)
if cls._in_readonly_mode():
raise ReadOnlyModeException()
return query
@classmethod
def update(cls, *args, **kwargs):
query = super(ReadReplicaSupportedModel, cls).update(*args, **kwargs)
if cls._in_readonly_mode():
raise ReadOnlyModeException()
return query
@classmethod
def delete(cls, *args, **kwargs):
query = super(ReadReplicaSupportedModel, cls).delete(*args, **kwargs)
if cls._in_readonly_mode():
raise ReadOnlyModeException()
return query
@classmethod
def raw(cls, *args, **kwargs):
query = super(ReadReplicaSupportedModel, cls).raw(*args, **kwargs)
if query._sql.lower().startswith("select "):
query._database = cls._select_database()
elif cls._in_readonly_mode():
raise ReadOnlyModeException()
elif query._sql.lower().startswith("insert "):
if is_deprecated_model(cls):
raise Exception("Attempt to write to deprecated model %s" % cls)
return query