|
1 | 1 | from collections.abc import Callable, Iterable |
2 | 2 | from copy import deepcopy |
3 | 3 | from http import HTTPStatus |
| 4 | +from time import sleep |
4 | 5 | from typing import Any, TypeVar, cast |
5 | 6 |
|
6 | 7 | import fastjsonschema |
7 | | -from psycopg.errors import RaiseException |
| 8 | +from psycopg.errors import RaiseException, SerializationFailure |
8 | 9 |
|
9 | 10 | from openslides_backend.services.database.extended_database import ExtendedDatabase |
10 | 11 | from openslides_backend.services.postgresql.db_connection_handling import ( |
11 | 12 | get_new_os_conn, |
12 | 13 | ) |
| 14 | +from openslides_backend.shared.exceptions import DatabaseException |
13 | 15 |
|
14 | 16 | from ..shared.exceptions import ( |
15 | 17 | ActionException, |
| 18 | + BadCodingException, |
16 | 19 | DatastoreLockedException, |
17 | 20 | RelationException, |
18 | 21 | View400Exception, |
@@ -122,52 +125,66 @@ def handle_request( |
122 | 125 | except fastjsonschema.JsonSchemaException as exception: |
123 | 126 | raise ActionException(exception.message) |
124 | 127 |
|
125 | | - try: |
126 | | - with get_new_os_conn() as conn: |
127 | | - self.post_edit_necessary = False |
128 | | - self.datastore = ExtendedDatabase(conn, self.logging, self.env) |
129 | | - results: ActionsResponseResults = [] |
130 | | - if atomic: |
131 | | - results = self.execute_write_requests( |
132 | | - self.parse_actions, payload |
133 | | - ) |
134 | | - else: |
135 | | - |
136 | | - def transform_to_list( |
137 | | - tuple: tuple[WriteRequest | None, ActionResults | None], |
138 | | - ) -> tuple[list[WriteRequest], ActionResults | None]: |
139 | | - return ( |
140 | | - [tuple[0]] if tuple[0] is not None else [], |
141 | | - tuple[1], |
| 128 | + retry_count = int(self.env.ACTION_MAX_RETRIES or 1) |
| 129 | + retry_timeout = float(self.env.ACTION_RETRY_TIMEOUT or 0.4) |
| 130 | + for attempt in range(1, retry_count + 1): |
| 131 | + try: |
| 132 | + with get_new_os_conn() as conn: |
| 133 | + self.post_edit_necessary = False |
| 134 | + self.datastore = ExtendedDatabase(conn, self.logging, self.env) |
| 135 | + results: ActionsResponseResults = [] |
| 136 | + if atomic: |
| 137 | + results = self.execute_write_requests( |
| 138 | + self.parse_actions, payload |
142 | 139 | ) |
143 | | - |
144 | | - for element in payload: |
145 | | - try: |
146 | | - result = self.execute_write_requests( |
147 | | - lambda e: transform_to_list(self.perform_action(e)), |
148 | | - element, |
| 140 | + else: |
| 141 | + |
| 142 | + def transform_to_list( |
| 143 | + tuple: tuple[WriteRequest | None, ActionResults | None], |
| 144 | + ) -> tuple[list[WriteRequest], ActionResults | None]: |
| 145 | + return ( |
| 146 | + [tuple[0]] if tuple[0] is not None else [], |
| 147 | + tuple[1], |
149 | 148 | ) |
150 | | - results.append(result) |
151 | | - except ActionException as exception: |
152 | | - error = cast(ActionError, exception.get_json()) |
153 | | - results.append(error) |
154 | | - self.datastore.reset() |
155 | | - |
156 | | - # execute cleanup methods |
157 | | - for on_success in self.on_success: |
158 | | - on_success() |
159 | | - |
160 | | - # Return action result |
161 | | - self.logger.info("Request was successful. Send response now.") |
162 | | - return ActionsResponse( |
163 | | - status_code=HTTPStatus.OK.value, |
164 | | - success=True, |
165 | | - message="Actions handled successfully", |
166 | | - results=results, |
| 149 | + |
| 150 | + for element in payload: |
| 151 | + try: |
| 152 | + result = self.execute_write_requests( |
| 153 | + lambda e: transform_to_list( |
| 154 | + self.perform_action(e) |
| 155 | + ), |
| 156 | + element, |
| 157 | + ) |
| 158 | + results.append(result) |
| 159 | + except ActionException as exception: |
| 160 | + error = cast(ActionError, exception.get_json()) |
| 161 | + results.append(error) |
| 162 | + self.datastore.reset() |
| 163 | + |
| 164 | + # execute cleanup methods |
| 165 | + for on_success in self.on_success: |
| 166 | + on_success() |
| 167 | + |
| 168 | + # Return action result |
| 169 | + self.logger.info("Request was successful. Send response now.") |
| 170 | + return ActionsResponse( |
| 171 | + status_code=HTTPStatus.OK.value, |
| 172 | + success=True, |
| 173 | + message="Actions handled successfully", |
| 174 | + results=results, |
| 175 | + ) |
| 176 | + except RaiseException as e: |
| 177 | + # This is raised at the end of transaction as the constraint trigger has to be initially deferred. |
| 178 | + raise RelationException( |
| 179 | + f"Relation violates required constraint: {e}" |
167 | 180 | ) |
168 | | - except RaiseException as e: |
169 | | - # This is raised at the end of transaction as the constraint trigger has to be initially deferred. |
170 | | - raise RelationException(f"Relation violates required constraint: {e}") |
| 181 | + except SerializationFailure: |
| 182 | + if attempt == retry_count: |
| 183 | + raise DatabaseException( |
| 184 | + "Database operation failed due to concurring actions. Please try again later." |
| 185 | + ) |
| 186 | + sleep(retry_timeout) |
| 187 | + raise BadCodingException("This code should never execute") |
171 | 188 |
|
172 | 189 | def execute_internal_action(self, action: str, data: dict[str, Any]) -> None: |
173 | 190 | """Helper function to execute an internal action with user id -1.""" |
|
0 commit comments