Skip to content

Commit

Permalink
PRMDR-785: Upload retry logic in the event of upload failure (#356)
Browse files Browse the repository at this point in the history
* Add Upload Retry logic in the event of upload failure

* fix pip-audit issues and format

* pr changes
  • Loading branch information
abbas-khan10 authored May 2, 2024
1 parent ad2c336 commit 29af215
Show file tree
Hide file tree
Showing 14 changed files with 100 additions and 87 deletions.
2 changes: 1 addition & 1 deletion app/src/helpers/requests/uploadDocument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('[POST] updateDocumentState', () => {
const document = buildDocument(buildTextFile('test1'), documentUploadStates.SUCCEEDED);
mockedAxios.post.mockImplementation(() => Promise.resolve({ status: 200 }));
const args = {
document,
documents: [document],
uploadingState: true,
documentReference: 'test/test/test/key',
baseUrl: '/test',
Expand Down
17 changes: 8 additions & 9 deletions app/src/helpers/requests/uploadDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type DocRefResponse = {
};

type UpdateStateArgs = {
document: UploadDocument;
documents: UploadDocument[];
uploadingState: boolean;
baseUrl: string;
baseHeaders: AuthHeaders;
Expand Down Expand Up @@ -187,21 +187,20 @@ const uploadDocuments = async ({
throw error;
}
};

export const updateDocumentState = async ({
document,
documents,
uploadingState,
baseUrl,
baseHeaders,
}: UpdateStateArgs) => {
const updateUploadStateUrl = baseUrl + endpoints.UPLOAD_DOCUMENT_STATE;
const body = {
files: [
{
reference: document.ref,
type: document.docType,
fields: { Uploading: uploadingState },
},
],
files: documents.map((document) => ({
reference: document.ref,
type: document.docType,
fields: { Uploading: uploadingState },
})),
};
try {
return await axios.post(updateUploadStateUrl, body, {
Expand Down
43 changes: 26 additions & 17 deletions app/src/pages/lloydGeorgeUploadPage/LloydGeorgeUploadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ function LloydGeorgeUploadPage() {
const hasNoVirus =
documents.length && documents.every((d) => d.state === DOCUMENT_UPLOAD_STATE.CLEAN);

const setUploadStateFailed = async () => {
await updateDocumentState({
documents: documents,
uploadingState: false,
baseUrl,
baseHeaders,
});
};

const confirmUpload = async () => {
if (uploadSession) {
setStage(LG_UPLOAD_STAGE.CONFIRMATION);
Expand All @@ -105,16 +114,19 @@ function LloydGeorgeUploadPage() {
navigate(routes.SESSION_EXPIRED);
return;
}
await setUploadStateFailed();
setStage(LG_UPLOAD_STAGE.FAILED);
}
}
};

if (hasExceededUploadAttempts) {
window.clearInterval(intervalTimer);
void setUploadStateFailed();
setStage(LG_UPLOAD_STAGE.FAILED);
} else if (hasVirus) {
window.clearInterval(intervalTimer);
void setUploadStateFailed();
setStage(LG_UPLOAD_STAGE.INFECTED);
} else if (hasNoVirus && !confirmed.current) {
confirmed.current = true;
Expand Down Expand Up @@ -161,19 +173,12 @@ function LloydGeorgeUploadPage() {
progress: 100,
});
} catch (e) {
window.clearInterval(intervalTimer);
setDocument(setDocuments, {
id: document.id,
state: DOCUMENT_UPLOAD_STATE.FAILED,
attempts: document.attempts + 1,
progress: 0,
});
await updateDocumentState({
document,
uploadingState: false,
baseUrl,
baseHeaders,
});
}
});
};
Expand Down Expand Up @@ -233,21 +238,25 @@ function LloydGeorgeUploadPage() {
setDocuments([]);
setStage(LG_UPLOAD_STAGE.SELECT);
};

const startIntervalTimer = (uploadDocuments: Array<UploadDocument>) => {
return window.setInterval(() => {
uploadDocuments.forEach(async (document) => {
try {
await updateDocumentState({
document,
uploadingState: true,
baseUrl,
baseHeaders,
});
} catch (e) {}
});
void setDocumentUploadingState(uploadDocuments, true);
}, 120000);
};

const setDocumentUploadingState = async (
uploadDocuments: Array<UploadDocument>,
uploadingState: boolean,
) => {
await updateDocumentState({
documents: uploadDocuments,
uploadingState: uploadingState,
baseUrl,
baseHeaders,
});
};

switch (stage) {
case LG_UPLOAD_STAGE.SELECT:
return (
Expand Down
1 change: 1 addition & 0 deletions lambdas/models/auth_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
"""

import os
import re

Expand Down
10 changes: 10 additions & 0 deletions lambdas/models/document_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
from utils.exceptions import InvalidDocumentReferenceException


class UploadDocumentReference(BaseModel):
reference: str = Field(...)
doc_type: str = Field(..., alias="type")
fields: dict[str, bool] = Field(...)


class UploadDocumentReferences(BaseModel):
files: list[UploadDocumentReference] = Field(...)


class DocumentReference(BaseModel):
id: str = Field(..., alias=str(DocumentReferenceMetadataFields.ID.value))
content_type: str = Field(
Expand Down
14 changes: 8 additions & 6 deletions lambdas/models/pds_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ def get_patient_details(self, nhs_number) -> PatientDetails:
givenName=self.get_current_usual_name().given,
familyName=self.get_current_usual_name().family,
birthDate=self.birth_date,
postalCode=self.get_current_home_address().postal_code
if self.is_unrestricted()
else "",
postalCode=(
self.get_current_home_address().postal_code
if self.is_unrestricted()
else ""
),
nhsNumber=self.id,
superseded=bool(nhs_number == id),
restricted=not self.is_unrestricted(),
Expand All @@ -128,9 +130,9 @@ def get_minimum_patient_details(self, nhs_number) -> PatientDetails:
givenName=self.get_current_usual_name().given,
familyName=self.get_current_usual_name().family,
birthDate=self.birth_date,
generalPracticeOds=self.get_active_ods_code_for_gp()
if self.is_unrestricted()
else "",
generalPracticeOds=(
self.get_active_ods_code_for_gp() if self.is_unrestricted() else ""
),
nhsNumber=self.id,
superseded=bool(nhs_number == id),
restricted=not self.is_unrestricted(),
Expand Down
4 changes: 2 additions & 2 deletions lambdas/models/staging_metadata.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, FieldValidationInfo, field_validator
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
from pydantic_core import PydanticCustomError

METADATA_FILENAME = "metadata.csv"
Expand Down Expand Up @@ -36,7 +36,7 @@ class MetadataFile(BaseModel):
@field_validator("gp_practice_code")
@classmethod
def ensure_gp_practice_code_non_empty(
cls, gp_practice_code: str, info: FieldValidationInfo
cls, gp_practice_code: str, info: ValidationInfo
) -> str:
if not gp_practice_code:
patient_nhs_number = info.data.get("nhs_number", "")
Expand Down
4 changes: 2 additions & 2 deletions lambdas/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ charset-normalizer==3.2.0
cryptography==42.0.4
email-validator==2.1.0.post1
fhir.resources[xml]==7.1.0
idna==3.4
idna==3.7
inflection==0.5.1
jmespath==1.0.1
oauthlib==3.2.2
packaging==23.0.0
pycparser==2.21
pydantic==2.3.0
pydantic==2.4.0
pypdf==3.17.2
python-dateutil==2.8.2
requests==2.31.0
Expand Down
6 changes: 3 additions & 3 deletions lambdas/scripts/batch_update_ods_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ def build_progress_dict(self, dynamodb_records: list[dict]) -> dict:
progress_dict[nhs_number].doc_ref_ids.append(doc_ref_id)
pds_code_at_current_row = ods_code
if progress_dict[nhs_number].prev_ods_code != pds_code_at_current_row:
progress_dict[
nhs_number
].prev_ods_code = "[multiple ods codes in records]"
progress_dict[nhs_number].prev_ods_code = (
"[multiple ods codes in records]"
)

self.logger.info(f"Totally {len(progress_dict)} patients found in record.")
return progress_dict
Expand Down
6 changes: 3 additions & 3 deletions lambdas/services/create_document_reference_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ def create_document_reference_request(
400, LambdaError.CreateDocInvalidType
)

url_responses[
document_reference.file_name
] = self.prepare_pre_signed_url(document_reference)
url_responses[document_reference.file_name] = (
self.prepare_pre_signed_url(document_reference)
)

if lg_documents:
validate_lg_files(lg_documents, nhs_number)
Expand Down
12 changes: 6 additions & 6 deletions lambdas/services/document_reference_search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ def get_document_references(self, nhs_number: str):
for table_name in list_of_table_names:
logger.info(f"Searching for results in {table_name}")

documents: list[
DocumentReference
] = self.fetch_documents_from_table_with_filter(
nhs_number,
table_name,
query_filter=delete_filter_expression,
documents: list[DocumentReference] = (
self.fetch_documents_from_table_with_filter(
nhs_number,
table_name,
query_filter=delete_filter_expression,
)
)

results.extend(
Expand Down
54 changes: 25 additions & 29 deletions lambdas/services/update_upload_state_service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import os
from datetime import datetime, timezone

from botocore.exceptions import ClientError
from enums.lambda_error import LambdaError
from enums.metadata_field_names import DocumentReferenceMetadataFields
from enums.supported_document_types import SupportedDocumentTypes
from models.document_reference import UploadDocumentReferences
from pydantic import ValidationError
from services.base.dynamo_service import DynamoDBService
from utils.audit_logging_setup import LoggingService
from utils.decorators.validate_document_type import (
doc_type_is_valid,
extract_document_type_as_enum,
)
from utils.lambda_exceptions import UpdateUploadStateException

logger = LoggingService(__name__)
Expand All @@ -18,50 +23,41 @@
class UpdateUploadStateService:
def __init__(self):
self.dynamo_service = DynamoDBService()
self.lg_dynamo_table = os.getenv("LLOYD_GEORGE_DYNAMODB_NAME")
self.arf_dynamo_table = os.getenv("DOCUMENT_STORE_DYNAMODB_NAME")

def handle_update_state(self, event_body: dict):
try:
files = event_body["files"]
for file in files:
doc_ref = file["reference"]
doc_type = file["type"]
uploaded = file["fields"][Fields.UPLOADING.value]
not_valid = (
not doc_type
or not doc_ref
or not uploaded
or not isinstance(uploaded, bool)
)
if not_valid:
raise UpdateUploadStateException(
400, LambdaError.UpdateUploadStateValidation
)
elif doc_type not in [
SupportedDocumentTypes.ARF.value,
SupportedDocumentTypes.LG.value,
]:
upload_document_references = UploadDocumentReferences.model_validate(
event_body
)
for file in upload_document_references.files:
if not doc_type_is_valid(file.doc_type):
raise UpdateUploadStateException(
400, LambdaError.UpdateUploadStateDocType
)
else:
self.update_document(doc_ref, doc_type, uploaded)

self.update_document(
file.reference,
extract_document_type_as_enum(file.doc_type),
file.fields[str(Fields.UPLOADING.value)],
)
except (KeyError, TypeError) as e:
logger.error(
f"{LambdaError.UpdateUploadStateKey.to_str()} :{str(e)}",
)
raise UpdateUploadStateException(400, LambdaError.UpdateUploadStateKey)
except ValidationError as e:
logger.error(
f"{LambdaError.UpdateUploadStateValidation.to_str()} :{str(e)}",
)
raise UpdateUploadStateException(
400, LambdaError.UpdateUploadStateValidation
)

def update_document(
self, doc_ref: str, doc_type: SupportedDocumentTypes, uploaded: bool
):
updated_fields = self.format_update({Fields.UPLOADING.value: uploaded})
table = (
self.lg_dynamo_table
if doc_type == SupportedDocumentTypes.LG.value
else self.arf_dynamo_table
)
table = SupportedDocumentTypes.get_dynamodb_table_name(doc_type)
try:
self.dynamo_service.update_item(
table_name=table,
Expand Down
8 changes: 3 additions & 5 deletions lambdas/tests/unit/helpers/data/update_upload_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,21 @@

MOCK_DOCUMENT_REFERENCE = TEST_FILE_KEY

MOCK_LG_DOCTYPE = SupportedDocumentTypes.LG.value
MOCK_LG_DOCUMENTS_REQUEST = {
"files": [
{
"reference": TEST_FILE_KEY,
"type": SupportedDocumentTypes.LG.value,
"type": str(SupportedDocumentTypes.LG.value),
"fields": {Fields.UPLOADING.value: True},
}
]
}

MOCK_ARF_DOCTYPE = SupportedDocumentTypes.ARF.value
MOCK_ARF_DOCUMENTS_REQUEST = {
"files": [
{
"reference": TEST_FILE_KEY,
"type": SupportedDocumentTypes.ARF.value,
"type": str(SupportedDocumentTypes.ARF.value),
"fields": {Fields.UPLOADING.value: True},
}
]
Expand All @@ -34,7 +32,7 @@
"files": [
{
"reference": TEST_FILE_KEY,
"type": SupportedDocumentTypes.ALL.value,
"type": str(SupportedDocumentTypes.ALL.value),
"fields": {Fields.UPLOADING.value: True},
}
]
Expand Down
Loading

0 comments on commit 29af215

Please sign in to comment.