mirror of
https://github.com/scylladb/scylladb.git
synced 2026-04-20 08:30:35 +00:00
466 lines
30 KiB
Python
466 lines
30 KiB
Python
# Copyright 2025-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
|
|
|
|
from contextlib import contextmanager, nullcontext
|
|
from functools import cache, wraps
|
|
from itertools import count
|
|
|
|
import pytest
|
|
from botocore.exceptions import ClientError
|
|
|
|
from test.alternator.util import random_string
|
|
from test.cqlpy.util import config_value_context
|
|
from test.alternator.test_metrics import metrics, get_metrics, get_metric
|
|
|
|
# This file contains tests for the Alternator expression caching.
|
|
# It tests caching for UpdateExpression, ConditionExpression and ProjectionExpression, based on the metrics:
|
|
# - scylla_alternator_expression_cache_misses,
|
|
# - scylla_alternator_expression_cache_hits,
|
|
# - scylla_alternator_expression_cache_evictions.
|
|
# It assumes that cache works, when metrics report expected number of events.
|
|
#
|
|
# As the cache is per shard, the caching events for each request rely on the shard that processes them.
|
|
# E.g after the first request's 'miss', the repeated request would 'hit' only when it is processed by the same shard.
|
|
# However cache functionality is tested in unit test. Here mainly we want to confirm that requests actually use cache.
|
|
#
|
|
# There are basic tests, checking minimum certain numbers of events, that work even if the shard handling
|
|
# the requests changes during the test.
|
|
#
|
|
# More detailed tests (checking exact numbers) strictly require that all requests are handled by the same shard.
|
|
# While in practice this is what happens (connection is reused), theoretically it is not guaranteed.
|
|
# To confirm that result is relyable, we check the number of operations reported by selected shard.
|
|
# If it did not get all requests, we repeat the tests.
|
|
|
|
# Context manager to temporarily change cache settings for the duration of the yield
|
|
# Note: It flushes the cache, by first setting it to 0, then to the desired size.
|
|
@contextmanager
|
|
def config_max_cache_entries_per_shard(cql, cache_size):
|
|
with config_value_context(cql, 'alternator_max_expression_cache_entries_per_shard', '0'):
|
|
with config_value_context(cql, 'alternator_max_expression_cache_entries_per_shard', str(cache_size)):
|
|
yield
|
|
|
|
# Checks if actual cache metric increase (misses, hits, evictions) during the scope of this context match given expected values.
|
|
# If misses, hits or evictions are None, they are not checked.
|
|
# Optionally it can also check increase in the number of operations - on error it raises `UnexpectedOperationCount`
|
|
# Labels are used to filter metrics, e.g. {'shard': 0, 'expression': 'UpdateExpression'}.
|
|
# Note: evictions and operations are filtered by shard label only (if given), as they are not per-expression.
|
|
# If labels are None, no filtering is done.
|
|
# If allow_higher_values is True, it checks that actual values are at least as high as expected.
|
|
@contextmanager
|
|
def cache_metrics_check(metrics, misses=None, hits=None, evictions=None, operations=None, labels=None, allow_higher_values=False):
|
|
metric_defs = { 'misses': {'name': 'scylla_alternator_expression_cache_misses', 'labels': labels},
|
|
'hits': {'name': 'scylla_alternator_expression_cache_hits', 'labels': labels},
|
|
'evictions': {'name': 'scylla_alternator_expression_cache_evictions', 'labels': {'shard': labels['shard']} if labels and 'shard' in labels else None},
|
|
'operations': {'name': 'scylla_alternator_operation', 'labels': {'shard': labels['shard']} if labels and 'shard' in labels else None},
|
|
'shard_bounce_for_lwt': {'name': 'scylla_alternator_shard_bounce_for_lwt', 'labels': {'shard': labels['shard']} if labels and 'shard' in labels else None} }
|
|
expected_metric_values = {'misses': misses, 'hits': hits, 'evictions': evictions, 'operations': operations, 'shard_bounce_for_lwt': 0}
|
|
expected_metric_values = {name: expected_metric_values[name] for name in metric_defs if expected_metric_values[name] is not None}
|
|
the_metrics = get_metrics(metrics + "?__help__=false&__name__=alternator_*") or "#"
|
|
actual_metric_values = {metric: get_metric(metrics, metric_defs[metric]['name'], metric_defs[metric]['labels'], the_metrics) for metric in expected_metric_values}
|
|
yield
|
|
the_metrics = get_metrics(metrics + "?__help__=false&__name__=alternator_*") or "#"
|
|
actual_metric_values = {metric: get_metric(metrics, metric_defs[metric]['name'], metric_defs[metric]['labels'], the_metrics) - actual_metric_values[metric] for metric in actual_metric_values}
|
|
cmp = lambda a, e: a >= e if allow_higher_values else a == e
|
|
if ("operations" in actual_metric_values):
|
|
actual_metric_values["operations"] += actual_metric_values["shard_bounce_for_lwt"]
|
|
actual_metric_values["shard_bounce_for_lwt"] = 0
|
|
if ("operations" in actual_metric_values and not cmp(actual_metric_values["operations"], expected_metric_values["operations"])):
|
|
raise UnexpectedOperationCount(
|
|
f"{metric_defs['operations']['labels']} expected {expected_metric_values['operations']} operations, got {actual_metric_values['operations']}"
|
|
)
|
|
assert all(cmp(actual_metric_values[metric], expected_metric_values[metric]) for metric in expected_metric_values), \
|
|
f'cache metrics: {actual_metric_values} changed not as expected: {expected_metric_values}'
|
|
|
|
class UnexpectedOperationCount(Exception):
|
|
pass
|
|
|
|
# Helper function that sets a new `value` in the table, by calling `UpdateItem` with UpdateExpression.
|
|
# Success is confirmed by getting an item from table.
|
|
# To succeed, `expression_variable` must be a valid expression 'variable' name to be used in ExpressionAttributeValues.
|
|
# By changing `expression_variable` we get a different expression string, which is a cache key.
|
|
def update_with_value(table, key, expression_variable, value):
|
|
table.update_item(Key={ 'p': key }, UpdateExpression=f'SET a = {expression_variable}', ExpressionAttributeValues={f'{expression_variable}': value})
|
|
assert table.get_item(Key={ 'p': key }, ConsistentRead=True)['Item']['a'] == value
|
|
|
|
# Helper function similar to `update_with_value`, but adding ConditionalExpression that check if new value is not equal to old one.
|
|
# `update_should_succeed` indicates whether condition should be met (new value is set) or not (new value was equal to old one) and a exception is expected.
|
|
def update_with_value_if_different(table, key, expression_variable, value, update_should_succeed):
|
|
with nullcontext() if update_should_succeed else pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
table.update_item(Key={ 'p': key }, UpdateExpression=f'SET a = {expression_variable}', ExpressionAttributeValues={f'{expression_variable}': value},
|
|
ConditionExpression=f'a <> {expression_variable}')
|
|
assert table.get_item(Key={ 'p': key }, ConsistentRead=True)['Item']['a'] == value
|
|
|
|
# Helper function to get the number of shards from metrics.
|
|
# It looks for the 'shard' label in the metrics and returns the maximum shard number found.
|
|
# If no shards are found, it returns 2 by default (as tests are run with "--smp 2" by default).
|
|
@cache
|
|
def scylla_shard_count(metrics):
|
|
import re
|
|
matches = re.findall(r',shard="(\d+)"', get_metrics(metrics))
|
|
assert matches, "No per-shard metrics found - could not determine shards count"
|
|
return 1+max(int(n) for n in matches)
|
|
|
|
# Basic test to check that the expressions are cached at all. It uses only UpdateExpression.
|
|
# It does not depend on the shard that handles each request - we measure sums of events from all shards
|
|
# and repeat requests to reach minimal number of events, even if each request is handled by a different shard.
|
|
def test_caching_basic_workflow(cql, metrics, test_table_s):
|
|
with config_max_cache_entries_per_shard(cql, 2):
|
|
p = random_string()
|
|
shards_count = scylla_shard_count(metrics)
|
|
counter = count(1)
|
|
|
|
with cache_metrics_check(metrics, evictions=0):
|
|
# All cache clean - must be a miss, no evictions
|
|
with cache_metrics_check(metrics, misses=1, allow_higher_values=True):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val1', value=next(counter))
|
|
|
|
# Repeat 'shards_count' times - at least one hit, no evictions
|
|
with cache_metrics_check(metrics, hits=1, allow_higher_values=True):
|
|
for _ in range(0, shards_count):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val1', value=next(counter))
|
|
|
|
# New pattern - must be a miss, no evictions
|
|
with cache_metrics_check(metrics, misses=1, allow_higher_values=True):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val2', value=next(counter))
|
|
|
|
# Repeat 'shards_count' times - at least one hit, no evictions
|
|
with cache_metrics_check(metrics, hits=1, allow_higher_values=True):
|
|
for _ in range(0, shards_count):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val2', value=next(counter))
|
|
|
|
# Basic test that cache has max size and entries are ever evicted.
|
|
# It does not depend on the shard that handles each request - we measure sums of events from all shards
|
|
# and repeat requests to reach minimal number of events, even if each request is handled by a different shard.
|
|
def test_caching_is_ever_evicted(cql, metrics, test_table_s):
|
|
cache_size = 2
|
|
with config_max_cache_entries_per_shard(cql, cache_size):
|
|
p = random_string()
|
|
shards_count = scylla_shard_count(metrics)
|
|
counter = count(1)
|
|
patterns = cache_size*shards_count + 1
|
|
with cache_metrics_check(metrics, misses=patterns, evictions=1, allow_higher_values=True):
|
|
for i in range(0, patterns):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=f':val{i}', value=next(counter))
|
|
|
|
# Basic test that all expressions in all requests are cached.
|
|
# It does not depend on the shard that handles each request - we measure sums of events from all shards.
|
|
# For each case we have a new expression and repeat requests 'shards_count' times to ensure at least one miss and one hit.
|
|
def test_caching_all_expressions(cql, metrics, test_table_s):
|
|
with config_max_cache_entries_per_shard(cql, 200):
|
|
p = random_string()
|
|
shards_count = scylla_shard_count(metrics)
|
|
counter = count(1)
|
|
|
|
# UpdateExpression
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "UpdateExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val_UE', value=next(counter))
|
|
|
|
# ConditionExpression in UpdateItem
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ConditionExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':val_CE_UI', value=next(counter), update_should_succeed=True)
|
|
|
|
# ConditionExpression in PutItem
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ConditionExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
v = next(counter)
|
|
test_table_s.put_item(Item={'p': p, 'a': v}, ConditionExpression=f'a <> :val_CE_PI', ExpressionAttributeValues={':val_CE_PI': v})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == v
|
|
|
|
# ConditionExpression in DeleteItem
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ConditionExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
test_table_s.delete_item(Key={'p': p}, ConditionExpression='a <> :val_CE_DI', ExpressionAttributeValues={':val_CE_DI': next(counter)})
|
|
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
|
|
v = next(counter)
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val_UE', value=v)
|
|
|
|
# ProjectionExpression in GetItem
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ProjectionExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a')['Item']['a'] == v
|
|
|
|
# ProjectionExpression in Scan
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ProjectionExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
assert [{'a': v}] == [{'a': x['a']} for x in test_table_s.scan(ConsistentRead=True, ProjectionExpression="p, a")['Items'] if x['p'] == p]
|
|
|
|
# ProjectionExpression in BatchGetItem
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ProjectionExpression"}, allow_higher_values=True):
|
|
args = { 'Keys': [{'p': p}], 'ProjectionExpression': '#a_BGI', 'ExpressionAttributeNames': {'#a_BGI': 'a'}, 'ConsistentRead': True }
|
|
for _ in range(0, shards_count+1):
|
|
assert [{'a': v}] == test_table_s.meta.client.batch_get_item(RequestItems = {test_table_s.name: args})['Responses'][test_table_s.name]
|
|
|
|
# ProjectionExpression in Query
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ProjectionExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
assert [{'a': v}] == test_table_s.query(ConsistentRead=True, ProjectionExpression='#a_Q1', ExpressionAttributeNames={'#a_Q1': 'a'},
|
|
KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})['Items']
|
|
|
|
# KeyConditionExpression and FilterExpression in Query
|
|
with cache_metrics_check(metrics, hits=1, misses=1, labels={'expression': "ConditionExpression"}, allow_higher_values=True):
|
|
for _ in range(0, shards_count+1):
|
|
assert [{'a': v}] == test_table_s.query(ConsistentRead=True, ExpressionAttributeValues={':p': p, ':a': v}, ExpressionAttributeNames={'#a': 'a'},
|
|
ProjectionExpression="#a", KeyConditionExpression='p=:p', FilterExpression='a=:a')['Items']
|
|
|
|
def get_connection_shard(metrics, table):
|
|
metric = metrics + "?__help__=false&__name__=alternator_operation"
|
|
def get_op_count(metrics, the_metrics, shard):
|
|
return get_metric(metrics, 'scylla_alternator_operation', {'shard': shard}, the_metrics)
|
|
the_metrics = get_metrics(metric)
|
|
saved_metrics = { x: get_op_count(metrics, the_metrics, x) for x in range(scylla_shard_count(metrics)) }
|
|
|
|
table.get_item(Key={'p': "key"}, ConsistentRead=True)
|
|
|
|
the_metrics = get_metrics(metric)
|
|
con_shard = [ shard for shard,value in saved_metrics.items()
|
|
if get_op_count(metrics, the_metrics, shard) - value == 1 ]
|
|
if len(con_shard) != 1:
|
|
pytest.fail(f'Expected exactly one shard to handle the requests, but found {len(con_shard)}: {con_shard}')
|
|
return con_shard[0]
|
|
|
|
def retrying_on_unexpected_operation_count(num_attempts=5):
|
|
def decorator(test_func):
|
|
@wraps(test_func)
|
|
def wrapper(*args, **kwargs):
|
|
last_exc = []
|
|
for _ in range(1, num_attempts + 1):
|
|
try:
|
|
return test_func(*args, **kwargs)
|
|
except UnexpectedOperationCount as exc:
|
|
last_exc.append(exc)
|
|
pytest.fail(f"Unexpected number of operations reached observed shard. All attempts failed: {last_exc}")
|
|
return wrapper
|
|
return decorator
|
|
|
|
# Tests that UpdateExpression patterns are cached correctly -
|
|
# the first time we see an expression, it is a miss, then we have hits for the same pattern.
|
|
# With full cache we have evictions along with misses.
|
|
# It strictly requires that all requests are handled by the same shard, as it checks exact values.
|
|
@retrying_on_unexpected_operation_count()
|
|
def test_caching_update_expression(cql, metrics, test_table_s):
|
|
shard = get_connection_shard(metrics, test_table_s)
|
|
with config_max_cache_entries_per_shard(cql, 2):
|
|
p = random_string()
|
|
labels = {'shard': shard, 'expression': "UpdateExpression"}
|
|
counter = count(1)
|
|
|
|
# Pattern 'A'
|
|
with cache_metrics_check(metrics, hits=0, misses=1, evictions=0, operations=2, labels=labels):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valA', value=next(counter))
|
|
with cache_metrics_check(metrics, hits=3, misses=0, evictions=0, operations=6, labels=labels):
|
|
for i in range(3):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valA', value=next(counter))
|
|
|
|
# Pattern 'B'
|
|
with cache_metrics_check(metrics, hits=0, misses=1, evictions=0, operations=2, labels=labels):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valB', value=next(counter))
|
|
with cache_metrics_check(metrics, hits=3, misses=0, evictions=0, operations=6, labels=labels):
|
|
for i in range(3):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valB', value=next(counter))
|
|
|
|
# Patterns 'A' and 'B' mixed
|
|
with cache_metrics_check(metrics, hits=10, misses=0, evictions=0, operations=20, labels=labels):
|
|
for i in range(5):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valA', value=next(counter))
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valB', value=next(counter))
|
|
|
|
# Patterns 0 - 9
|
|
with cache_metrics_check(metrics, hits=0, misses=10, evictions=10, operations=20, labels=labels):
|
|
for i in range(10):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=f':val{i}', value=next(counter))
|
|
|
|
# Tests that ConditionExpression patterns are cached correctly from UpdateItme, PutItem, DeleteItem.
|
|
# The first time we see an expression, it is a miss, then we have hits for the same pattern.
|
|
# With full cache we have evictions along with misses.
|
|
# It strictly requires that all requests are handled by the same shard, as it checks exact values.
|
|
@retrying_on_unexpected_operation_count()
|
|
def test_caching_condition_expression(cql, metrics, test_table_s):
|
|
shard = get_connection_shard(metrics, test_table_s)
|
|
with config_max_cache_entries_per_shard(cql, 2):
|
|
p = random_string()
|
|
labels = {'shard': shard, 'expression': "ConditionExpression"}
|
|
counter = count(1)
|
|
|
|
# UpdateItem - it uses also UpdateExpression, so there are more evictions.
|
|
# Pattern 'A'
|
|
with cache_metrics_check(metrics, hits=0, misses=1, evictions=0, operations=2, labels=labels):
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valA', value=next(counter), update_should_succeed=True)
|
|
with cache_metrics_check(metrics, hits=6, misses=0, evictions=0, operations=12, labels=labels):
|
|
for _ in range(3):
|
|
v = next(counter)
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valA', value=v, update_should_succeed=True)
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valA', value=v, update_should_succeed=False)
|
|
|
|
# Pattern 'B'
|
|
with cache_metrics_check(metrics, hits=0, misses=1, evictions=2, operations=2, labels=labels):
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valB', value=next(counter), update_should_succeed=True)
|
|
with cache_metrics_check(metrics, hits=6, misses=0, evictions=0, operations=12, labels=labels):
|
|
for i in range(3):
|
|
v = next(counter)
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valB', value=v, update_should_succeed=True)
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valB', value=v, update_should_succeed=False)
|
|
|
|
# Patterns 'A' and 'B' mixed
|
|
with cache_metrics_check(metrics, hits=0, misses=6, evictions=12, operations=12, labels=labels):
|
|
for i in range(3):
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valA', value=next(counter), update_should_succeed=True)
|
|
update_with_value_if_different(table=test_table_s, key=p, expression_variable=':valB', value=next(counter), update_should_succeed=True)
|
|
|
|
# PutItem/DeleteItem
|
|
with cache_metrics_check(metrics, hits=10, misses=2, evictions=2, operations=21, labels=labels):
|
|
for _ in range(3):
|
|
v = next(counter)
|
|
# put_item expression_pattern='C'
|
|
test_table_s.put_item(Item={'p': p, 'a': v}, ConditionExpression=f'a <> :valC', ExpressionAttributeValues={':valC': v})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == v
|
|
# put_item expression_pattern='D'
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.put_item(Item={'p': p, 'a': v}, ConditionExpression=f'a <> :valD', ExpressionAttributeValues={':valD': v})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == v
|
|
|
|
# delete_item expression_pattern='C'
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.delete_item(Key={'p': p}, ConditionExpression='a <> :valC', ExpressionAttributeValues={':valC': v})
|
|
# delete_item expression_pattern='D'
|
|
test_table_s.delete_item(Key={'p': p}, ConditionExpression='a <> :valD', ExpressionAttributeValues={':valD': next(counter)})
|
|
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
|
|
# Tests that ProjectionExpression patterns are cached correctly from GetItem, Scan, Query, BatchGetItem.
|
|
# The first time we see an expression, it is a miss, then we have hits for the same pattern.
|
|
# With full cache we have evictions along with misses.
|
|
# It strictly requires that all requests are handled by the same shard, as it checks exact values.
|
|
@retrying_on_unexpected_operation_count()
|
|
def test_caching_projection_expression(cql, metrics, test_table_s):
|
|
shard = get_connection_shard(metrics, test_table_s)
|
|
with config_max_cache_entries_per_shard(cql, 2):
|
|
p = random_string()
|
|
labels = {'shard': shard, 'expression': "ProjectionExpression"}
|
|
ret = {'a': 'hello'}
|
|
|
|
# fill cache
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valA', value="hi")
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valB', value="hello")
|
|
|
|
# GetItem
|
|
with cache_metrics_check(metrics, hits=5, misses=1, evictions=1, operations=6, labels=labels):
|
|
for _ in range(6):
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a')['Item'] == ret
|
|
|
|
with cache_metrics_check(metrics, hits=5, misses=1, evictions=1, operations=6, labels=labels):
|
|
for _ in range(6):
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#name', ExpressionAttributeNames={'#name': 'a'})['Item'] == ret
|
|
|
|
with cache_metrics_check(metrics, hits=6, misses=0, evictions=0, operations=6, labels=labels):
|
|
for _ in range(3):
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#name', ExpressionAttributeNames={'#name': 'a'})['Item'] == ret
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a')['Item'] == ret
|
|
|
|
# Scan/Query/BatchGetItem
|
|
with cache_metrics_check(metrics, hits=8, misses=1, evictions=1, operations=9, labels=labels):
|
|
for _ in range(3):
|
|
assert [ret] == [ {'a': x['a']} for x in test_table_s.scan(ConsistentRead=True, ProjectionExpression="p, a")['Items'] if x['p'] == p]
|
|
assert [ret] == test_table_s.query(ConsistentRead=True, ProjectionExpression="a",
|
|
KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})['Items']
|
|
assert [ret] == test_table_s.meta.client.batch_get_item(RequestItems = {test_table_s.name: {
|
|
'Keys': [{'p': p}], 'ProjectionExpression': 'a', 'ConsistentRead': True}})['Responses'][test_table_s.name]
|
|
|
|
# Test Query separately with all three possible expressions: KeyConditionExpression, FilterExpression and ProjectionExpression.
|
|
# It strictly requires that all requests are handled by the same shard, as it checks exact values.
|
|
@retrying_on_unexpected_operation_count()
|
|
def test_caching_query_expression(cql, metrics, test_table_s):
|
|
shard = get_connection_shard(metrics, test_table_s)
|
|
with config_max_cache_entries_per_shard(cql, 3):
|
|
p = random_string()
|
|
labels = {'shard': shard}
|
|
ret = {'a': 'hello'}
|
|
|
|
# fill cache
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valA', value="hi")
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valB', value="hello")
|
|
|
|
# Query
|
|
with cache_metrics_check(metrics, hits=6, misses=3, evictions=2, operations=3, labels=labels):
|
|
for _ in range(3):
|
|
assert [ret] == test_table_s.query(ConsistentRead=True, ExpressionAttributeValues={':p': p, ':a': 'hello'},
|
|
ProjectionExpression="a", KeyConditionExpression='p=:p', FilterExpression='a=:a')['Items']
|
|
|
|
# Test that validation errors will be reported correctly and will not affect cache.
|
|
# It strictly requires that all requests are handled by the same shard, as it checks exact values.
|
|
@retrying_on_unexpected_operation_count()
|
|
def test_caching_invalid_expression(cql, metrics, test_table_s):
|
|
shard = get_connection_shard(metrics, test_table_s)
|
|
with config_max_cache_entries_per_shard(cql, 2):
|
|
p = random_string()
|
|
labels = {'shard': shard}
|
|
counter = count(1)
|
|
|
|
# fill cache
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valA', value=next(counter))
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valB', value=next(counter))
|
|
|
|
# Try invalid requests
|
|
# We don't check 'misses' here as it is debatable if they should be counted.
|
|
with cache_metrics_check(metrics, hits=0, evictions=0, operations=3, labels=labels):
|
|
with pytest.raises(ClientError, match='ValidationException'): #UpdateExpression
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val#@*', value=next(counter))
|
|
with pytest.raises(ClientError, match='ValidationException'): #ConditionExpression
|
|
v = next(counter)
|
|
test_table_s.put_item(Item={'p': p, 'a': v}, ConditionExpression=f'a <> :val#@*', ExpressionAttributeValues={':val#@*': v})
|
|
with pytest.raises(ClientError, match='ValidationException'): #ProjectionExpression
|
|
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='^@*')
|
|
|
|
# Check that cache is still working
|
|
with cache_metrics_check(metrics, hits=10, misses=0, evictions=0, operations=20, labels=labels):
|
|
for _ in range(5):
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valA', value=next(counter))
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':valB', value=next(counter))
|
|
|
|
# Test that each shard has its own cache, that is used for separate connections to the same shard,
|
|
# but not shared between connections to different shards.
|
|
# It strictly requires control over which shards processes each request.
|
|
@retrying_on_unexpected_operation_count()
|
|
def test_caching_on_shards(cql, metrics, test_table_s, new_dynamodb_session):
|
|
p = random_string()
|
|
update_with_value(table=test_table_s, key=p, expression_variable=':val', value="test")
|
|
with config_max_cache_entries_per_shard(cql, 2):
|
|
connection_tables1 = [None] * scylla_shard_count(metrics)
|
|
connection_tables2 = [None] * scylla_shard_count(metrics)
|
|
# Depending on the shard assignment policy in the system,
|
|
# in case we get a shard we don't want to, if we drop it immediately,
|
|
# we may get it again in the next try. Keeping it should increase chances of getting all shards.
|
|
to_drop = []
|
|
connection_tables1[get_connection_shard(metrics, test_table_s)] = test_table_s
|
|
setup_retries = 100*len(connection_tables1)
|
|
while (None in connection_tables1 or None in connection_tables2):
|
|
setup_retries -= 1
|
|
if setup_retries == 0:
|
|
print(connection_tables1)
|
|
print(connection_tables2)
|
|
pytest.fail("Failed to setup connections to all shards.")
|
|
t = new_dynamodb_session().Table(test_table_s.name)
|
|
s = get_connection_shard(metrics, t)
|
|
print(s)
|
|
if connection_tables1[s] is None:
|
|
connection_tables1[s] = t
|
|
elif connection_tables2[s] is None:
|
|
connection_tables2[s] = t
|
|
else:
|
|
to_drop.append(t)
|
|
|
|
def try_projection_expression(table, expression_variable):
|
|
assert {'a': "test"} == table.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=expression_variable,
|
|
ExpressionAttributeNames={expression_variable: 'a'})['Item']
|
|
|
|
for shard, table in enumerate(connection_tables1):
|
|
labels = {'shard': shard}
|
|
with cache_metrics_check(metrics, hits=18, misses=2, evictions=0, operations=20, labels=labels):
|
|
for _ in range(5):
|
|
try_projection_expression(table, '#valA')
|
|
try_projection_expression(table, f'#val{shard}')
|
|
try_projection_expression(connection_tables2[shard], '#valA')
|
|
try_projection_expression(connection_tables2[shard], f'#val{shard}')
|