The Alternator CreateTable operation currently performs several schema- changing operations separately - one by one: It creates a keyspace, a table in that keyspace and possibly also multiple views, and it sets tags on the table. A consequence of this is that concurrent CreateTable and DeleteTable operations (for example) can result in unexpected errors or inconsistent states - for example CreateTable wants to create the table in the keyspace it just created, but a concurrent DeleteTable deleted it. We have two issues about this problem (#6391 and #9868) and three tests (test_table.py::test_concurrent_create_and_delete_table) reproducing it. In this patch we fix these problems by switching to the modern Scylla schema-changing API: Instead of doing several schema-changing operations one by one, we create a vector of schema mutation performing all these operations - and then perform all these mutations together. When the experimental Raft-based schema modifications is enabled, this completely solves the races, and the tests begin to pass. However, if the experimental Raft mode is not enabled, these tests continue to fail because there is still no locking while applying the different schema mutations (not even on a single node). So I put a special fixture "fails_without_raft" on these tests - which means that the tests xfail if run without raft, and expected to pass when run on Raft. Indeed, after this patch test/alternator/run --raft test_table.py::test_concurrent_create_and_delete_table shows three passing tests (they also pass if we drastically improve the number of iterations), while test/alternator/run test_table.py::test_concurrent_create_and_delete_table shows three xfailing tests. All other Alternator tests pass as before with this patch, verifying that the handling of new tables, new views, tags, and CDC log tables, all happen correctly even after this patch. A note about the implementation: Before this patch, the CreateTable code used high-level functions like prepare_new_column_family_announcement(). These high-level functions become unusable if we write multiple schema operations to one list of mutations, because for example this function validates that the keyspace had already been created - when it hasn't and that's the whole point. So instead we had to use lower-level function like add_table_or_view_to_schema_mutation() and before_create_column_family(). However, despite being lower level, these functions were public so I think it's reasonable to use them, and we probably have no other alternative. Fixes #6391 Fixes #9868 Signed-off-by: Nadav Har'El <nyh@scylladb.com>
487 lines
22 KiB
Python
487 lines
22 KiB
Python
# Copyright 2019-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
# Tests for basic table operations: CreateTable, DeleteTable, ListTables.
|
|
# Also some basic tests for UpdateTable - although UpdateTable usually
|
|
# enables more elaborate features (such as GSI or Streams) and those are
|
|
# tested elsewhere.
|
|
|
|
import pytest
|
|
import time
|
|
import threading
|
|
from botocore.exceptions import ClientError
|
|
from util import list_tables, unique_table_name, create_test_table, random_string
|
|
|
|
# Utility function for create a table with a given name and some valid
|
|
# schema.. This function initiates the table's creation, but doesn't
|
|
# wait for the table to actually become ready.
|
|
def create_table(dynamodb, name, BillingMode='PAY_PER_REQUEST', **kwargs):
|
|
return dynamodb.create_table(
|
|
TableName=name,
|
|
BillingMode=BillingMode,
|
|
KeySchema=[
|
|
{
|
|
'AttributeName': 'p',
|
|
'KeyType': 'HASH'
|
|
},
|
|
{
|
|
'AttributeName': 'c',
|
|
'KeyType': 'RANGE'
|
|
}
|
|
],
|
|
AttributeDefinitions=[
|
|
{
|
|
'AttributeName': 'p',
|
|
'AttributeType': 'S'
|
|
},
|
|
{
|
|
'AttributeName': 'c',
|
|
'AttributeType': 'S'
|
|
},
|
|
],
|
|
**kwargs
|
|
)
|
|
|
|
# Utility function for creating a table with a given name, and then deleting
|
|
# it immediately, waiting for these operations to complete. Since the wait
|
|
# uses DescribeTable, this function requires all of CreateTable, DescribeTable
|
|
# and DeleteTable to work correctly.
|
|
# Note that in DynamoDB, table deletion takes a very long time, so tests
|
|
# successfully using this function are very slow.
|
|
def create_and_delete_table(dynamodb, name, **kwargs):
|
|
table = create_table(dynamodb, name, **kwargs)
|
|
table.meta.client.get_waiter('table_exists').wait(TableName=name)
|
|
table.delete()
|
|
table.meta.client.get_waiter('table_not_exists').wait(TableName=name)
|
|
|
|
##############################################################################
|
|
|
|
# Test creating a table, and then deleting it, waiting for each operation
|
|
# to have completed before proceeding. Since the wait uses DescribeTable,
|
|
# this tests requires all of CreateTable, DescribeTable and DeleteTable to
|
|
# function properly in their basic use cases.
|
|
# Unfortunately, this test is extremely slow with DynamoDB because deleting
|
|
# a table is extremely slow until it really happens.
|
|
def test_create_and_delete_table(dynamodb):
|
|
create_and_delete_table(dynamodb, 'alternator_test')
|
|
|
|
# Test that recreating a table right after deleting it works without issues
|
|
def test_recreate_table(dynamodb):
|
|
create_and_delete_table(dynamodb, 'alternator_recr_test')
|
|
create_and_delete_table(dynamodb, 'alternator_recr_test')
|
|
|
|
# DynamoDB documentation specifies that table names must be 3-255 characters,
|
|
# and match the regex [a-zA-Z0-9._-]+. Names not matching these rules should
|
|
# be rejected, and no table be created.
|
|
def test_create_table_unsupported_names(dynamodb):
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, 'n')
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, 'nn')
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, 'n' * 256)
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, 'nyh@test')
|
|
|
|
# On the other hand, names following the above rules should be accepted. Even
|
|
# names which the Scylla rules forbid, such as a name starting with .
|
|
def test_create_and_delete_table_non_scylla_name(dynamodb):
|
|
create_and_delete_table(dynamodb, '.alternator_test')
|
|
|
|
# names with 255 characters are allowed in Dynamo, but they are not currently
|
|
# supported in Scylla because we create a directory whose name is the table's
|
|
# name followed by 33 bytes (underscore and UUID). So currently, we only
|
|
# correctly support names with length up to 222.
|
|
def test_create_and_delete_table_very_long_name(dynamodb):
|
|
# In the future, this should work:
|
|
#create_and_delete_table(dynamodb, 'n' * 255)
|
|
# But for now, only 222 works:
|
|
create_and_delete_table(dynamodb, 'n' * 222)
|
|
# We cannot test the following on DynamoDB because it will succeed
|
|
# (DynamoDB allows up to 255 bytes)
|
|
#with pytest.raises(ClientError, match='ValidationException'):
|
|
# create_table(dynamodb, 'n' * 223)
|
|
|
|
# Tests creating a table with an invalid schema should return a
|
|
# ValidationException error.
|
|
def test_create_table_invalid_schema(dynamodb):
|
|
# The name of the table "created" by this test shouldn't matter, the
|
|
# creation should not succeed anyway.
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.create_table(
|
|
TableName='name_doesnt_matter',
|
|
BillingMode='PAY_PER_REQUEST',
|
|
KeySchema=[
|
|
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'c', 'KeyType': 'HASH' }
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
|
],
|
|
)
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.create_table(
|
|
TableName='name_doesnt_matter',
|
|
BillingMode='PAY_PER_REQUEST',
|
|
KeySchema=[
|
|
{ 'AttributeName': 'p', 'KeyType': 'RANGE' },
|
|
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
|
],
|
|
)
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.create_table(
|
|
TableName='name_doesnt_matter',
|
|
BillingMode='PAY_PER_REQUEST',
|
|
KeySchema=[
|
|
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
|
],
|
|
)
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.create_table(
|
|
TableName='name_doesnt_matter',
|
|
BillingMode='PAY_PER_REQUEST',
|
|
KeySchema=[
|
|
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'p', 'KeyType': 'RANGE' },
|
|
{ 'AttributeName': 'z', 'KeyType': 'RANGE' }
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'z', 'AttributeType': 'S' }
|
|
],
|
|
)
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.create_table(
|
|
TableName='name_doesnt_matter',
|
|
BillingMode='PAY_PER_REQUEST',
|
|
KeySchema=[
|
|
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'z', 'AttributeType': 'S' }
|
|
],
|
|
)
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.create_table(
|
|
TableName='name_doesnt_matter',
|
|
BillingMode='PAY_PER_REQUEST',
|
|
KeySchema=[
|
|
{ 'AttributeName': 'k', 'KeyType': 'HASH' },
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'k', 'AttributeType': 'Q' }
|
|
],
|
|
)
|
|
|
|
# Test that trying to create a table that already exists fails in the
|
|
# appropriate way (ResourceInUseException)
|
|
def test_create_table_already_exists(dynamodb, test_table):
|
|
with pytest.raises(ClientError, match='ResourceInUseException.*Table.*already exists'):
|
|
create_table(dynamodb, test_table.name)
|
|
|
|
# Test that BillingMode error path works as expected - only the values
|
|
# PROVISIONED or PAY_PER_REQUEST are allowed. The former requires
|
|
# ProvisionedThroughput to be set, the latter forbids it.
|
|
# If BillingMode is outright missing, it defaults (as original
|
|
# DynamoDB did) to PROVISIONED so ProvisionedThroughput is allowed.
|
|
def test_create_table_billing_mode_errors(dynamodb, test_table):
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, unique_table_name(), BillingMode='unknown')
|
|
# billing mode is case-sensitive
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, unique_table_name(), BillingMode='pay_per_request')
|
|
# PAY_PER_REQUEST cannot come with a ProvisionedThroughput:
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, unique_table_name(),
|
|
BillingMode='PAY_PER_REQUEST', ProvisionedThroughput={'ReadCapacityUnits': 10, 'WriteCapacityUnits': 10})
|
|
# On the other hand, PROVISIONED requires ProvisionedThroughput:
|
|
# By the way, ProvisionedThroughput not only needs to appear, it must
|
|
# have both ReadCapacityUnits and WriteCapacityUnits - but we can't test
|
|
# this with boto3, because boto3 has its own verification that if
|
|
# ProvisionedThroughput is given, it must have the correct form.
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
create_table(dynamodb, unique_table_name(), BillingMode='PROVISIONED')
|
|
# If BillingMode is completely missing, it defaults to PROVISIONED, so
|
|
# ProvisionedThroughput is required
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.create_table(TableName=unique_table_name(),
|
|
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
|
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }])
|
|
|
|
# Even before Alternator gains full support for the DynamoDB stream API
|
|
# and CreateTable's StreamSpecification option, we should support the
|
|
# options which mean it is turned *off*.
|
|
def test_table_streams_off(dynamodb):
|
|
# If StreamSpecification is given, but has StreamEnabled=false, it's as
|
|
# if StreamSpecification was missing. StreamViewType isn't needed.
|
|
table = create_test_table(dynamodb, StreamSpecification={'StreamEnabled': False},
|
|
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
|
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
|
table.delete();
|
|
# DynamoDB doesn't allow StreamSpecification to be empty map - if it
|
|
# exists, it must have a StreamEnabled
|
|
# Unfortunately, new versions of boto3 doesn't let us pass this...
|
|
#with pytest.raises(ClientError, match='ValidationException'):
|
|
# table = create_test_table(dynamodb, StreamSpecification={},
|
|
# KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
|
# AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
|
# table.delete();
|
|
# Unfortunately, boto3 doesn't allow us to pass StreamSpecification=None.
|
|
# This is what we had in issue #5796.
|
|
|
|
# For tests with 'StreamEnabled': True, and different 'StreamViewType', see
|
|
# test_streams.py.
|
|
|
|
# Our first implementation had a special column name called "attrs" where
|
|
# we stored a map for all non-key columns. If the user tried to name one
|
|
# of the key columns with this same name, the result was a disaster - Scylla
|
|
# goes into a bad state after trying to write data with two updates to same-
|
|
# named columns.
|
|
special_column_name1 = 'attrs'
|
|
special_column_name2 = ':attrs'
|
|
@pytest.fixture(scope="module")
|
|
def test_table_special_column_name(dynamodb):
|
|
table = create_test_table(dynamodb,
|
|
KeySchema=[
|
|
{ 'AttributeName': special_column_name1, 'KeyType': 'HASH' },
|
|
{ 'AttributeName': special_column_name2, 'KeyType': 'RANGE' }
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': special_column_name1, 'AttributeType': 'S' },
|
|
{ 'AttributeName': special_column_name2, 'AttributeType': 'S' },
|
|
],
|
|
)
|
|
yield table
|
|
table.delete()
|
|
@pytest.mark.xfail(reason="special attrs column not yet hidden correctly")
|
|
def test_create_table_special_column_name(test_table_special_column_name):
|
|
s = random_string()
|
|
c = random_string()
|
|
h = random_string()
|
|
expected = {special_column_name1: s, special_column_name2: c, 'hello': h}
|
|
test_table_special_column_name.put_item(Item=expected)
|
|
got = test_table_special_column_name.get_item(Key={special_column_name1: s, special_column_name2: c}, ConsistentRead=True)['Item']
|
|
assert got == expected
|
|
|
|
# Test that all tables we create are listed, and pagination works properly.
|
|
# Note that the DyanamoDB setup we run this against may have hundreds of
|
|
# other tables, for all we know. We just need to check that the tables we
|
|
# created are indeed listed.
|
|
def test_list_tables_paginated(dynamodb, test_table, test_table_s, test_table_b):
|
|
my_tables_set = {table.name for table in [test_table, test_table_s, test_table_b]}
|
|
for limit in [1, 2, 3, 4, 50, 100]:
|
|
print("testing limit={}".format(limit))
|
|
list_tables_set = set(list_tables(dynamodb, limit))
|
|
assert my_tables_set.issubset(list_tables_set)
|
|
|
|
# Test that pagination limit is validated
|
|
def test_list_tables_wrong_limit(dynamodb):
|
|
# lower limit (min. 1) is imposed by boto3 library checks
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
dynamodb.meta.client.list_tables(Limit=101)
|
|
|
|
# Even before Alternator gains support for configuring server-side encryption
|
|
# ("encryption at rest") with CreateTable's SSESpecification option, we should
|
|
# support the option "Enabled=false" which is the default, and means the server
|
|
# takes care of whatever server-side encryption is done, on its own.
|
|
# Reproduces issue #7031.
|
|
def test_table_sse_off(dynamodb):
|
|
# If StreamSpecification is given, but has StreamEnabled=false, it's as
|
|
# if StreamSpecification was missing, and fine. No other attribues are
|
|
# necessary.
|
|
table = create_test_table(dynamodb, SSESpecification = {'Enabled': False},
|
|
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
|
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
|
table.delete();
|
|
|
|
# Test that trying to delete a table that doesn't exist fails in the
|
|
# appropriate way (ResourceNotFoundException)
|
|
def test_delete_table_non_existent(dynamodb, test_table):
|
|
client = dynamodb.meta.client
|
|
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
|
client.delete_table(TableName=random_string(20))
|
|
|
|
# Test that trying to update a table that doesn't exist fails in the
|
|
# appropriate way (ResourceNotFoundException)
|
|
def test_update_table_non_existent(dynamodb, test_table):
|
|
client = dynamodb.meta.client
|
|
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
|
client.update_table(TableName=random_string(20), BillingMode='PAY_PER_REQUEST')
|
|
|
|
# While the raft-based schema modifications are still experimental and only
|
|
# optionally enabled (use the "--raft" option to test/alternator/run to
|
|
# enable it), some tests are expected to fail on Scylla without this option
|
|
# enabled, and pass with it enabled (and also pass on DynamoDB). These tests
|
|
# should use the "fails_without_raft" fixture. When Raft mode becomes the
|
|
# default, this fixture can be removed.
|
|
@pytest.fixture(scope="session")
|
|
def check_pre_raft(dynamodb):
|
|
from util import is_aws
|
|
# If not running on Scylla, return false.
|
|
if is_aws(dynamodb):
|
|
return false
|
|
# In Scylla, we check Raft mode by inspecting the configuration via a
|
|
# system table (which is also visible in Alternator)
|
|
config_table = dynamodb.Table('.scylla.alternator.system.config')
|
|
experimental_features = config_table.query(
|
|
KeyConditionExpression='#key=:val',
|
|
ExpressionAttributeNames={'#key': 'name'},
|
|
ExpressionAttributeValues={':val': 'experimental_features'}
|
|
)['Items'][0]['value']
|
|
return not '"raft"' in experimental_features
|
|
@pytest.fixture(scope="function")
|
|
def fails_without_raft(request, check_pre_raft):
|
|
if check_pre_raft:
|
|
request.node.add_marker(pytest.mark.xfail(reason='Test expected to fail without Raft experimental feature on'))
|
|
|
|
# Test for reproducing issues #6391 and #9868 - where CreateTable did not
|
|
# *atomically* perform all the schema modifications - creating a keyspace,
|
|
# a table, secondary indexes and tags - and instead it created the different
|
|
# pieces one after another. In that case it is possible that if we
|
|
# concurrently create and delete the same table, the deletion may, for
|
|
# example, delete the table just created and then creating the secondary
|
|
# index in it would fail.
|
|
#
|
|
# In each test we'll have two threads - one looping repeatedly creating
|
|
# the same table, and the second looping repeatedly deleting this table.
|
|
# It is expected for the table creation to fail because the table already
|
|
# exists, and for the table deletion to fail because the table doesn't
|
|
# exist. But what we don't want to see is a different kind failure (e.g.,
|
|
# InternalServerError) in the *middle* of the table creation or deletion.
|
|
# Such a failure may even leave behind some half-created table.
|
|
#
|
|
# NOTE: This test, like all cql-pytest tests, runs on a single node. So it
|
|
# doesn't exercise the most general possibility that two concurrent schema
|
|
# modifications from two different coordinators collide. So multi-node
|
|
# tests will be needed to check for that potential problem as well.
|
|
# But even on one coordinator this test can already reproduce the bugs
|
|
# described in #6391 and #9868, so it's worth having this single-node test
|
|
# as well.
|
|
@pytest.mark.parametrize("table_def", [
|
|
# A table without a secondary index - requiring the creation of
|
|
# a keyspace and a table in it.
|
|
{ 'KeySchema': [ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
|
'AttributeDefinitions': [
|
|
{ 'AttributeName': 'p', 'AttributeType': 'S' }]
|
|
},
|
|
# Reproducer for #9868 - CreateTable needs to create a keyspace,
|
|
# a table, and a materialized view.
|
|
{ 'KeySchema': [ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
|
],
|
|
'AttributeDefinitions': [
|
|
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
|
],
|
|
'GlobalSecondaryIndexes': [
|
|
{ 'IndexName': 'hello',
|
|
'KeySchema': [
|
|
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'p', 'KeyType': 'RANGE' },
|
|
],
|
|
'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]
|
|
},
|
|
# Reproducer for #6391, a table with tags - which we used to add
|
|
# non-atomically after having already created the table.
|
|
{ 'KeySchema': [ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
|
'AttributeDefinitions': [
|
|
{ 'AttributeName': 'p', 'AttributeType': 'S' }],
|
|
'Tags': [{'Key': 'k1', 'Value': 'v1'}]
|
|
}
|
|
])
|
|
def test_concurrent_create_and_delete_table(dynamodb, table_def, fails_without_raft):
|
|
# According to boto3 documentation, "Unlike Resources and Sessions,
|
|
# clients are generally thread-safe.". So because we have two threads
|
|
# in this test, we must not use "dynamodb" (containing the boto3
|
|
# "resource") - we should only the boto3 "client":
|
|
client = dynamodb.meta.client
|
|
# Unfortunately by default Python threads print their exceptions
|
|
# (e.g., assertion failures) but don't propagate them to the join(),
|
|
# so the overall test doesn't fail. The following Thread wrapper
|
|
# causes join() to rethrow the exception, so the test will fail.
|
|
class ThreadWrapper(threading.Thread):
|
|
def run(self):
|
|
try:
|
|
self.ret = self._target(*self._args, **self._kwargs)
|
|
except BaseException as e:
|
|
self.exception = e
|
|
def join(self, timeout=None):
|
|
super().join(timeout)
|
|
if hasattr(self, 'exception'):
|
|
raise self.exception
|
|
return self.ret
|
|
|
|
table_name = unique_table_name()
|
|
# The more iterations we do, the higher the chance of reproducing
|
|
# this issue. On my laptop, count = 10 reproduces the bug every time.
|
|
# Lower numbers have some chance of not catching the bug. If this
|
|
# issue starts to xpass, we may need to increase the count.
|
|
count = 10
|
|
def deletes():
|
|
for i in range(count):
|
|
try:
|
|
client.delete_table(TableName=table_name)
|
|
except Exception as e:
|
|
# Expect either success or a ResourceNotFoundException.
|
|
# On DynamoDB we can also get a ResourceInUseException
|
|
# if we try to delete a table while it's in the middle
|
|
# of being created.
|
|
# Anything else (e.g., InternalServerError) is a bug.
|
|
assert isinstance(e, ClientError) and (
|
|
'ResourceNotFoundException' in str(e) or
|
|
'ResourceInUseException' in str(e))
|
|
else:
|
|
print("delete successful")
|
|
def creates():
|
|
for i in range(count):
|
|
try:
|
|
client.create_table(TableName=table_name,
|
|
BillingMode='PAY_PER_REQUEST',
|
|
**table_def)
|
|
except Exception as e:
|
|
# Expect either success or a ResourceInUseException.
|
|
# Anything else (e.g., InternalServerError) is a bug.
|
|
assert isinstance(e, ClientError) and 'ResourceInUseException' in str(e)
|
|
else:
|
|
print("create successful")
|
|
t1 = ThreadWrapper(target=deletes)
|
|
t2 = ThreadWrapper(target=creates)
|
|
t1.start()
|
|
t2.start()
|
|
try:
|
|
t1.join()
|
|
t2.join()
|
|
finally:
|
|
# Make sure that in any case, the table is deleted before the
|
|
# test finishes. On DynamoDB, we can't just call DeleteTable -
|
|
# if some CreateTable is still in progress we can't call
|
|
# DeleteTable until it finishes...
|
|
timeout = time.time() + 120
|
|
while time.time() < timeout:
|
|
try:
|
|
client.delete_table(TableName=table_name)
|
|
break
|
|
except ClientError as e:
|
|
if 'ResourceNotFoundException' in str(e):
|
|
# The table was already deleted by the deletion thread,
|
|
# nothing left to do :-)
|
|
break
|
|
if 'ResourceInUseException' in str(e):
|
|
# A CreateTable opereration is still in progress,
|
|
# we can't delete the table yet.
|
|
time.sleep(1)
|
|
continue
|
|
raise
|