blob: 9e17fa4015a7566899f6b97070c7302df5fd10d7 [file] [log] [blame]
# Copyright (C) 2019 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import calendar
import json
import time
from cassandra.cqlengine import columns
from cassandra.cqlengine.models import Model
from collections import Iterable, OrderedDict
from datetime import datetime
from resultsdbpy.controller.configuration import Configuration
from resultsdbpy.model.commit_context import CommitContext
class ClusteredByConfiguration(Model):
platform = columns.Text(partition_key=True, required=True)
version = columns.Integer(partition_key=True, required=True)
is_simulator = columns.Boolean(partition_key=True, required=True)
architecture = columns.Text(partition_key=True, required=True)
attributes = columns.Text(partition_key=True, required=True)
def to_configuration(self):
return Configuration(
platform=self.platform,
version=self.version,
is_simulator=self.is_simulator,
architecture=self.architecture,
sdk=getattr(self, 'sdk', None) or None,
**json.loads(self.attributes)
)
class ConfigurationContext(object):
class ConfigurationModel(object):
def to_configuration(self):
return Configuration(
platform=self.platform,
version=self.version, version_name=self.version_name or None,
sdk=getattr(self, 'sdk', None) or None,
is_simulator=self.is_simulator,
architecture=self.architecture, model=self.model or None,
style=self.style or None, flavor=self.flavor or None,
)
class ByPlatform(Model, ConfigurationModel):
__table_name__ = 'configs_by_platform_with_branch'
platform = columns.Text(partition_key=True, required=True)
style = columns.Text(primary_key=True, required=False)
flavor = columns.Text(primary_key=True, required=False)
architecture = columns.Text(primary_key=True, required=True)
model = columns.Text(primary_key=True, required=False)
is_simulator = columns.Boolean(primary_key=True, required=True)
version = columns.Integer(primary_key=True, required=True)
branch = columns.Text(primary_key=True, required=True)
version_name = columns.Text(primary_key=True, required=False)
last_run = columns.DateTime(required=True)
class ByPlatformAndVersion(Model, ConfigurationModel):
__table_name__ = 'configs_by_platform_and_version_with_branch'
platform = columns.Text(partition_key=True, required=True)
version = columns.Integer(partition_key=True, required=True)
style = columns.Text(primary_key=True, required=False)
flavor = columns.Text(primary_key=True, required=False)
architecture = columns.Text(primary_key=True, required=True)
model = columns.Text(primary_key=True, required=False)
is_simulator = columns.Boolean(primary_key=True, required=True)
branch = columns.Text(primary_key=True, required=True)
version_name = columns.Text(primary_key=True, required=False)
last_run = columns.DateTime(required=True)
class ByArchitecture(Model, ConfigurationModel):
__table_name__ = 'configs_by_architecture_with_branch'
architecture = columns.Text(partition_key=True, required=True)
style = columns.Text(primary_key=True, required=False)
platform = columns.Text(primary_key=True, required=True)
version = columns.Integer(primary_key=True, required=True)
model = columns.Text(primary_key=True, required=False)
flavor = columns.Text(primary_key=True, required=False)
is_simulator = columns.Boolean(primary_key=True, required=True)
branch = columns.Text(primary_key=True, required=True)
version_name = columns.Text(primary_key=True, required=False)
last_run = columns.DateTime(required=True)
class ByModel(Model, ConfigurationModel):
__table_name__ = 'configs_by_model_with_branch'
model = columns.Text(partition_key=True, required=True)
version = columns.Integer(primary_key=True, required=True)
style = columns.Text(primary_key=True, required=False)
platform = columns.Text(primary_key=True, required=True)
flavor = columns.Text(primary_key=True, required=False)
architecture = columns.Text(primary_key=True, required=True)
is_simulator = columns.Boolean(primary_key=True, required=True)
branch = columns.Text(primary_key=True, required=True)
version_name = columns.Text(primary_key=True, required=False)
last_run = columns.DateTime(required=True)
def __init__(self, redis, cassandra, cache_timeout=60 * 60 * 24 * 14):
assert redis
assert cassandra
self.redis = redis
self.cassandra = cassandra
self.cache_timeout = cache_timeout
with self:
self.cassandra.create_table(self.ByPlatform)
self.cassandra.create_table(self.ByPlatformAndVersion)
self.cassandra.create_table(self.ByArchitecture)
self.cassandra.create_table(self.ByModel)
for configuration in self.cassandra.select_from_table(self.ByPlatform.__table_name__):
if configuration.last_run >= datetime.utcfromtimestamp(int(time.time() - cache_timeout)):
self._register_in_redis(configuration.to_configuration(), configuration.branch, configuration.last_run)
def __enter__(self):
self.cassandra.__enter__()
def __exit__(self, *args, **kwargs):
self.cassandra.__exit__(*args, **kwargs)
@classmethod
def _convert_to_redis_key(cls, configuration, branch):
return 'configs_with_branch:{}:{}:{}:{}:{}:{}:{}:{}:{}'.format(
configuration.platform or '*', '*' if configuration.is_simulator is None else (1 if configuration.is_simulator else 0),
'*' if configuration.version is None else Configuration.integer_to_version(configuration.version),
configuration.version_name or '*',
configuration.architecture or '*', configuration.model or '*',
configuration.style or '*', configuration.flavor or '*',
branch,
)
def _register_in_redis(self, configuration, branch, timestamp):
if isinstance(timestamp, datetime):
timestamp = calendar.timegm(timestamp.timetuple())
expiration = int(self.cache_timeout - (time.time() - int(timestamp)))
if expiration > 0:
sdk = configuration.sdk
try:
configuration.sdk = None
self.redis.set(self._convert_to_redis_key(configuration, branch), configuration.to_json(), ex=expiration)
finally:
configuration.sdk = sdk
def register_configuration(self, configuration, branch=None, timestamp=time.time()):
if not isinstance(configuration, Configuration):
raise TypeError(f'Expected type {Configuration}, got {type(configuration)}')
if not configuration.is_complete():
raise TypeError('Cannot register a partial configuration')
if not isinstance(timestamp, datetime):
timestamp = datetime.utcfromtimestamp(int(timestamp))
branch = branch or CommitContext.DEFAULT_BRANCH_KEY
with self:
tables_to_insert_to = [self.ByPlatform.__table_name__, self.ByPlatformAndVersion.__table_name__, self.ByArchitecture.__table_name__]
if configuration.model is not None:
tables_to_insert_to.append(self.ByModel.__table_name__)
for table in tables_to_insert_to:
self.cassandra.insert_row(
table, branch=branch,
platform=configuration.platform, is_simulator=configuration.is_simulator,
version=configuration.version, version_name=configuration.version_name or '',
architecture=configuration.architecture, model=configuration.model or '',
style=configuration.style or '', flavor=configuration.flavor or '',
last_run=timestamp,
)
self._register_in_redis(configuration, branch, timestamp)
def search_for_configuration(self, configuration=Configuration(), branch=None):
if not isinstance(configuration, Configuration):
raise TypeError(f'Expected type {Configuration}, got {type(configuration)}')
kwargs = dict(branch=branch or CommitContext.DEFAULT_BRANCH_KEY)
for member in Configuration.REQUIRED_MEMBERS + Configuration.OPTIONAL_MEMBERS:
if getattr(configuration, member):
kwargs[member] = getattr(configuration, member)
if 'platform' in kwargs and 'version' in kwargs:
table = self.ByPlatformAndVersion
elif 'platform' in kwargs:
table = self.ByPlatform
elif 'architecture' in kwargs:
table = self.ByArchitecture
elif 'model' in kwargs:
table = self.ByModel
else:
# Platforms rarely expire, so we can do a decent job of wildcard matching expired configurations
# if we try all platforms in the cache.
platforms = set([config.platform for config in self.search_for_recent_configuration(branch=branch)])
with self:
result = []
for platform in platforms:
configuration.platform = platform
result.extend(self.search_for_configuration(configuration, branch))
configuration.platform = None
return result
with self:
return [model.to_configuration() for model in self.cassandra.select_from_table(table.__table_name__, **kwargs)]
def search_for_recent_configuration(self, configuration=Configuration(), branch=None):
if not isinstance(configuration, Configuration):
raise TypeError(f'Expected type {Configuration}, got {type(configuration)}')
configurations = []
for key in self.redis.scan_iter(self._convert_to_redis_key(configuration, branch or CommitContext.DEFAULT_BRANCH_KEY)):
candidate = Configuration.from_json(self.redis.get(key.decode('utf-8')).decode('utf-8'))
if candidate == configuration:
configurations.append(candidate)
return configurations
def insert_row_with_configuration(self, table_name, configuration, **kwargs):
if not isinstance(configuration, Configuration):
raise TypeError(f'Expected type {Configuration}, got {type(configuration)}')
if not configuration.is_complete():
raise TypeError(f'Cannot insert to {table_name} with a partial configuration')
with self:
attributes_dict = OrderedDict()
for member in Configuration.OPTIONAL_MEMBERS:
if getattr(configuration, member) is not None:
attributes_dict[member] = getattr(configuration, member)
return self.cassandra.insert_row(
table_name,
platform=configuration.platform, is_simulator=configuration.is_simulator,
version=configuration.version,
architecture=configuration.architecture,
attributes=json.dumps(attributes_dict),
**kwargs)
def select_from_table_with_configurations(self, table_name, configurations=None, branch=None, recent=True, limit=100, **kwargs):
if not isinstance(configurations, Iterable):
raise TypeError('Expected configurations to be iterable')
if not configurations:
configurations.append(Configuration())
with self:
complete_configurations = set()
for config in configurations:
if not isinstance(config, Configuration):
raise TypeError(f'Expected type {Configuration}, got {type(config)}')
if config.is_complete():
complete_configurations.add(config)
elif recent:
[complete_configurations.add(element) for element in self.search_for_recent_configuration(config, branch=branch)]
else:
[complete_configurations.add(element) for element in self.search_for_configuration(config, branch=branch)]
results = {}
for configuration in complete_configurations:
attributes_dict = OrderedDict()
for member in Configuration.OPTIONAL_MEMBERS:
if getattr(configuration, member) is not None:
attributes_dict[member] = getattr(configuration, member)
rows = self.cassandra.select_from_table(
table_name,
platform=configuration.platform, is_simulator=configuration.is_simulator,
version=configuration.version,
architecture=configuration.architecture,
attributes=json.dumps(attributes_dict),
limit=limit,
branch=branch or CommitContext.DEFAULT_BRANCH_KEY,
**kwargs)
if len(rows) == 0:
continue
for row in rows:
full_config = row.to_configuration()
if not results.get(full_config):
results[full_config] = []
results[full_config].append(row)
return results