Skip to content

Commit 050b9a7

Browse files
authored
Merge pull request #1414 from leggedrobotics/dev
Release 0.43.4
2 parents e689c73 + 51dfa3e commit 050b9a7

File tree

12 files changed

+132
-56
lines changed

12 files changed

+132
-56
lines changed

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kleinkram-backend",
3-
"version": "0.43.3",
3+
"version": "0.43.4",
44
"description": "",
55
"author": "",
66
"private": true,

backend/src/endpoints/file/file.controller.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
} from '@common/api/types/file/access.dto';
5555

5656
import { FileQueryDto } from '@common/api/types/file/file-query.dto';
57+
import { CancelFileUploadDto } from '@common/api/types/cancel-file-upload.dto';
5758

5859
@Controller(['file', 'files']) // TODO: migrate to 'files'
5960
export class FileController {
@@ -286,20 +287,15 @@ export class FileController {
286287

287288
@Post('cancelUpload')
288289
@UserOnly() //Push back authentication to the queue to accelerate the request
289-
// TODO: type API response
290+
@OutputDto(null) // TODO: type API response
290291
async cancelUpload(
291-
@BodyUUIDArray(
292-
'uuids',
293-
"File UUIDs who, if they aren't 'OK', are deleted",
294-
)
295-
uuids: string[],
296-
@BodyUUID('missionUUID', 'Mission UUID') missionUUID: string,
292+
@Body() dto: CancelFileUploadDto,
297293
@AddUser() auth: AuthHeader,
298-
) {
299-
logger.debug(`cancelUpload ${uuids.toString()}`);
300-
return this.fileService.cancelUpload(
301-
uuids,
302-
missionUUID,
294+
): Promise<void> {
295+
logger.debug(`cancelUpload ${dto.uuids.toString()}`);
296+
await this.fileService.cancelUpload(
297+
dto.uuids,
298+
dto.missionUuid,
303299
auth.user.uuid,
304300
);
305301
}

backend/src/routing/middlewares/version-checker-middleware.service.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ export class VersionCheckerMiddlewareService implements NestMiddleware {
4949
}
5050

5151
// forbidden client versions: allows for the following notations
52-
// - '<0.40.x': versions below 0.40.x are forbidden
53-
// - '0.41.x': version 0.41.x is forbidden
54-
const forbiddenClientVersions = ['<0.40.0', '0.41.0', '0.41.1'];
52+
const forbiddenClientVersions = ['<0.43.4'];
5553

5654
if (this.isVersionForbidden(clientVersion, forbiddenClientVersions)) {
5755
this.rejectRequest(response, clientVersion);

cli/kleinkram/api/file_transfer.py

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import annotations
22

3+
import signal
34
import logging
45
import sys
56
from concurrent.futures import Future
67
from concurrent.futures import ThreadPoolExecutor
78
from concurrent.futures import as_completed
89
from enum import Enum
910
from pathlib import Path
10-
from time import monotonic
11+
from time import monotonic, sleep
1112
from typing import Dict
1213
from typing import NamedTuple
1314
from typing import Optional
@@ -39,6 +40,7 @@
3940
DOWNLOAD_CHUNK_SIZE = 1024 * 1024 * 16
4041
DOWNLOAD_URL = "/files/download"
4142

43+
MAX_UPLOAD_RETRIES = 3
4244
S3_MAX_RETRIES = 60 # same as frontend
4345
S3_READ_TIMEOUT = 60 * 5 # 5 minutes
4446

@@ -66,8 +68,8 @@ def _cancel_file_upload(
6668
client: AuthenticatedClient, file_id: UUID, mission_id: UUID
6769
) -> None:
6870
data = {
69-
"uuid": [str(file_id)],
70-
"missionUUID": str(mission_id),
71+
"uuids": [str(file_id)],
72+
"missionUuid": str(mission_id),
7173
}
7274
resp = client.post(UPLOAD_CANCEL, json=data)
7375
resp.raise_for_status()
@@ -151,6 +153,37 @@ class UploadState(Enum):
151153
CANCELED = 3
152154

153155

156+
def _get_upload_credentials_with_retry(
157+
client, pbar, filename, mission_id, max_attempts=5
158+
):
159+
"""
160+
Retrieves upload credentials with retry logic.
161+
162+
Args:
163+
client: The client object used for retrieving credentials.
164+
filename: The internal filename.
165+
mission_id: The mission ID.
166+
max_attempts: Maximum number of retry attempts.
167+
168+
Returns:
169+
The upload credentials or None if retrieval fails after all attempts.
170+
"""
171+
attempt = 0
172+
while attempt < max_attempts:
173+
creds = _get_upload_creditials(
174+
client, internal_filename=filename, mission_id=mission_id
175+
)
176+
if creds is not None:
177+
return creds
178+
179+
attempt += 1
180+
if attempt < max_attempts:
181+
delay = 2**attempt # Exponential backoff (2, 4, 8, 16...)
182+
sleep(delay)
183+
184+
return None
185+
186+
154187
# TODO: i dont want to handle errors at this level
155188
def upload_file(
156189
client: AuthenticatedClient,
@@ -161,40 +194,54 @@ def upload_file(
161194
verbose: bool = False,
162195
s3_endpoint: Optional[str] = None,
163196
) -> Tuple[UploadState, int]:
164-
"""\
197+
"""
165198
returns UploadState and bytes uploaded (0 if not uploaded)
199+
Retries up to 3 times on failure.
166200
"""
167201
if s3_endpoint is None:
168202
s3_endpoint = get_config().endpoint.s3
169203

170204
total_size = path.stat().st_size
171-
with tqdm(
172-
total=total_size,
173-
unit="B",
174-
unit_scale=True,
175-
desc=f"uploading {path}...",
176-
leave=False,
177-
disable=not verbose,
178-
) as pbar:
179-
# get per file upload credentials
180-
creds = _get_upload_creditials(
181-
client, internal_filename=filename, mission_id=mission_id
182-
)
183-
if creds is None:
184-
return UploadState.EXISTS, 0
205+
for attempt in range(MAX_UPLOAD_RETRIES):
206+
with tqdm(
207+
total=total_size,
208+
unit="B",
209+
unit_scale=True,
210+
desc=f"uploading {path}...",
211+
leave=False,
212+
disable=not verbose,
213+
) as pbar:
214+
215+
# get per file upload credentials
216+
creds = _get_upload_credentials_with_retry(
217+
client, pbar, filename, mission_id, max_attempts=5 if attempt > 0 else 1
218+
)
219+
220+
if creds is None:
221+
return UploadState.EXISTS, 0
185222

186-
try:
187-
_s3_upload(path, endpoint=s3_endpoint, credentials=creds, pbar=pbar)
188-
except Exception as e:
189-
logger.error(format_traceback(e))
190223
try:
191-
_cancel_file_upload(client, creds.file_id, mission_id)
192-
except Exception as cancel_e:
193-
logger.error(f"Failed to cancel upload for {creds.file_id}: {cancel_e}")
194-
raise e from e
195-
else:
196-
_confirm_file_upload(client, creds.file_id, b64_md5(path))
197-
return UploadState.UPLOADED, total_size
224+
_s3_upload(path, endpoint=s3_endpoint, credentials=creds, pbar=pbar)
225+
except Exception as e:
226+
logger.error(format_traceback(e))
227+
try:
228+
_cancel_file_upload(client, creds.file_id, mission_id)
229+
except Exception as cancel_e:
230+
logger.error(
231+
f"Failed to cancel upload for {creds.file_id}: {cancel_e}"
232+
)
233+
234+
if attempt < 2: # Retry if not the last attempt
235+
pbar.update(0)
236+
logger.error(f"Retrying upload for {attempt + 1}")
237+
continue
238+
else:
239+
logger.error(f"Cancelling upload for {attempt}")
240+
raise e from e
241+
242+
else:
243+
_confirm_file_upload(client, creds.file_id, b64_md5(path))
244+
return UploadState.UPLOADED, total_size
198245

199246

200247
def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
@@ -420,6 +467,9 @@ def upload_files(
420467
) as pbar:
421468
start = monotonic()
422469
futures: Dict[Future[Tuple[UploadState, int]], Path] = {}
470+
471+
skipped_files = 0
472+
failed_files = 0
423473
with ThreadPoolExecutor(max_workers=n_workers) as executor:
424474
for name, path in files.items():
425475
if not path.is_file():
@@ -441,6 +491,16 @@ def upload_files(
441491

442492
total_uploaded_bytes = 0
443493
for future in as_completed(futures):
494+
495+
if future.exception():
496+
failed_files += 1
497+
498+
if (
499+
future.exception() is None
500+
and future.result()[0] == UploadState.EXISTS
501+
):
502+
skipped_files += 1
503+
444504
path = futures[future]
445505
uploaded_bytes = _upload_handler(future, path, verbose=verbose)
446506
total_uploaded_bytes += uploaded_bytes
@@ -455,6 +515,16 @@ def upload_files(
455515
console.print(f"Total uploaded: {format_bytes(total_uploaded_bytes)}")
456516
console.print(f"Average speed: {format_bytes(avg_speed_bps, speed=True)}")
457517

518+
if failed_files > 0:
519+
console.print(
520+
f"\nUploaded {len(files) - failed_files - skipped_files} files, {skipped_files} skipped, {failed_files} uploads failed",
521+
style="red",
522+
)
523+
else:
524+
console.print(
525+
f"\nUploaded {len(files) - skipped_files} files, {skipped_files} skipped"
526+
)
527+
458528

459529
def download_files(
460530
client: AuthenticatedClient,

cli/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = kleinkram
3-
version = 0.43.3
3+
version = 0.43.4
44
description = give me your bags
55
long_description = file: README.md
66
long_description_content_type = text/markdown
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { IsArray, IsString, IsUUID } from 'class-validator';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
4+
export class CancelFileUploadDto {
5+
@ApiProperty()
6+
@IsArray()
7+
@IsString({ each: true })
8+
uuids!: string[];
9+
10+
@IsUUID()
11+
missionUuid!: string;
12+
}

common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kleinkram-common",
3-
"version": "0.43.3",
3+
"version": "0.43.4",
44
"license": "MIT",
55
"scripts": {
66
"seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config",

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kleinkram-docs",
3-
"version": "0.43.3",
3+
"version": "0.43.4",
44
"license": "MIT",
55
"target": "es2022",
66
"scripts": {

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kleinkram-frontend",
3-
"version": "0.43.3",
3+
"version": "0.43.4",
44
"description": "Data storage of ROS bags",
55
"productName": "Kleinkram",
66
"author": "Johann Schwabe <[email protected]>",

frontend/src/services/mutations/file.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ export const generateTemporaryCredentials = async (
5252
};
5353

5454
export const cancelUploads = async (
55-
fileUUIDs: string[],
56-
missionUUID: string,
57-
) => {
55+
fileUuids: string[],
56+
missionUuid: string,
57+
): Promise<void> => {
5858
const response = await axios.post('/file/cancelUpload', {
59-
uuids: fileUUIDs,
60-
missionUUID,
59+
uuids: fileUuids,
60+
missionUuid: missionUuid,
6161
});
6262
return response.data;
6363
};

0 commit comments

Comments
 (0)