Repository: celery/celery

Issue:
dynamoDB result backend incorrect exception handling when table exists
The following code

https://github.com/celery/celery/blob/d0f5300691ca594f2311daf542aa63367622c027/celery/backends/dynamodb.py#L192

tries to create or get the table. In case the table exists and the role executing that code on AWS does not have the `CreateTable` permission, the raised exception is not the one expected by that particular line. Yet the table exists _and_ the exception is raised because the code tries to create it while lacking permissions.

To reproduce:


* celery 5.0.5
* create the result backend table and give the following permissions to the role executing celery on that table:

```
   "dynamodb:DescribeTable",
   "dynamodb:PutItem",
   "dynamodb:UpdateItem",
   "dynamodb:DeleteItem",
   "dynamodb:BatchWriteItem",
   "dynamodb:GetItem",
   "dynamodb:BatchGetItem",
   "dynamodb:Scan",
   "dynamodb:Query",
   "dynamodb:ConditionCheckItem"
```

* remove the permission CreateTable from the role executing the code

Possible solution:

* handle the check of the table existence with another boto3 call such as [`describe_table`](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.describe_table) and then fall-back into the table creation
* provide a configuration that, when indicated, assumes the existence of the table
* handle that exception differently




Discussion:
> Possible solution:
> 
>     * handle the check of the table existence with another boto3 call such as [`describe_table`](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.describe_table) and then fall-back into the table creation
> 
>     * provide a configuration that, when indicated, assumes the existence of the table
> 
>     * handle that exception differently

I am no expert on dynamo DB. But if you can come with a change request as your proposed possible solutions we could verify & help to improve.
@galCohen88 can you try to fix this? 
@lyajedi let's continue here

Resolving patch:
diff --git a/celery/backends/dynamodb.py b/celery/backends/dynamodb.py
--- a/celery/backends/dynamodb.py
+++ b/celery/backends/dynamodb.py
@@ -201,28 +201,25 @@ def _get_or_create_table(self):
         """Create table if not exists, otherwise return the description."""
         table_schema = self._get_table_schema()
         try:
-            table_description = self._client.create_table(**table_schema)
-            logger.info(
-                'DynamoDB Table {} did not exist, creating.'.format(
-                    self.table_name
-                )
-            )
-            # In case we created the table, wait until it becomes available.
-            self._wait_for_table_status('ACTIVE')
-            logger.info(
-                'DynamoDB Table {} is now available.'.format(
-                    self.table_name
-                )
-            )
-            return table_description
+            return self._client.describe_table(TableName=self.table_name)
         except ClientError as e:
             error_code = e.response['Error'].get('Code', 'Unknown')
 
-            # If table exists, do not fail, just return the description.
-            if error_code == 'ResourceInUseException':
-                return self._client.describe_table(
-                    TableName=self.table_name
+            if error_code == 'ResourceNotFoundException':
+                table_description = self._client.create_table(**table_schema)
+                logger.info(
+                    'DynamoDB Table {} did not exist, creating.'.format(
+                        self.table_name
+                    )
+                )
+                # In case we created the table, wait until it becomes available.
+                self._wait_for_table_status('ACTIVE')
+                logger.info(
+                    'DynamoDB Table {} is now available.'.format(
+                        self.table_name
+                    )
                 )
+                return table_description
             else:
                 raise e
 


Test patch:
diff --git a/t/unit/backends/test_dynamodb.py b/t/unit/backends/test_dynamodb.py
--- a/t/unit/backends/test_dynamodb.py
+++ b/t/unit/backends/test_dynamodb.py
@@ -121,39 +121,34 @@ def test_get_client_time_to_live_called(
         mock_set_table_ttl.assert_called_once()
 
     def test_get_or_create_table_not_exists(self):
+        from botocore.exceptions import ClientError
+
         self.backend._client = MagicMock()
         mock_create_table = self.backend._client.create_table = MagicMock()
+        client_error = ClientError(
+            {
+                'Error': {
+                    'Code': 'ResourceNotFoundException'
+                }
+            },
+            'DescribeTable'
+        )
         mock_describe_table = self.backend._client.describe_table = \
             MagicMock()
-
-        mock_describe_table.return_value = {
-            'Table': {
-                'TableStatus': 'ACTIVE'
-            }
-        }
+        mock_describe_table.side_effect = client_error
+        self.backend._wait_for_table_status = MagicMock()
 
         self.backend._get_or_create_table()
+        mock_describe_table.assert_called_once_with(
+            TableName=self.backend.table_name
+        )
         mock_create_table.assert_called_once_with(
             **self.backend._get_table_schema()
         )
 
     def test_get_or_create_table_already_exists(self):
-        from botocore.exceptions import ClientError
-
         self.backend._client = MagicMock()
         mock_create_table = self.backend._client.create_table = MagicMock()
-        client_error = ClientError(
-            {
-                'Error': {
-                    'Code': 'ResourceInUseException',
-                    'Message': 'Table already exists: {}'.format(
-                        self.backend.table_name
-                    )
-                }
-            },
-            'CreateTable'
-        )
-        mock_create_table.side_effect = client_error
         mock_describe_table = self.backend._client.describe_table = \
             MagicMock()
 
@@ -167,6 +162,7 @@ def test_get_or_create_table_already_exists(self):
         mock_describe_table.assert_called_once_with(
             TableName=self.backend.table_name
         )
+        mock_create_table.assert_not_called()
 
     def test_wait_for_table_status(self):
         self.backend._client = MagicMock()