Как разделить методы для начала загрузки и передачи частей файла
Что проверяют
Умеете ли вы спроектировать загрузку больших файлов частями: отдельный вызов для старта, для каждой части и для завершения. Следом обычно спрашивают, что будет при обрыве связи на середине и как не держать весь файл в памяти.
Как отвечать
Разделите протокол на шаги с явным состоянием между ними:
InitiateUploadAsync(fileName, size, ct): сервер создаёт сессию загрузки и возвращаетuploadId, размер части и срок жизни сессии;UploadChunkAsync(uploadId, index, stream, ct): принимает одну часть. Метод идемпотентен: повторная отправка части с тем же номером перезаписывает её, а не портит файл. К части полезно прикладывать хеш;CompleteUploadAsync(uploadId, ct): сервер проверяет, что пришли все части, собирает файл и сверяет общий хеш;AbortUploadAsync(uploadId, ct): отмена, плюс фоновая чистка брошенных сессий по сроку.
Так устроены S3 multipart upload и протокол tus, на них удобно сослаться. Состояние сессии хранится вне процесса, в базе или Redis, иначе рестарт или второй экземпляр сервиса потеряют загрузку.
Детали на C#:
- все методы асинхронные и принимают
CancellationToken; - файл читается потоком:
FileStreamсFileOptions.Asynchronousи буфер изArrayPool<byte>.Shared. Массив от 85 000 байт попадает в Large Object Heap, аFile.ReadAllBytesна гигабайтном файле быстро упрётся в память; - части можно слать параллельно через
Parallel.ForEachAsyncс ограничениемMaxDegreeOfParallelism, а повторы при сетевых ошибках вынести в политику Polly; - Kestrel по умолчанию ограничивает тело запроса примерно 30 МБ (
MaxRequestBodySize), и это ещё один довод резать файл.
Где спотыкаются
Делают один метод, который и открывает файл, и шлёт его целиком. Обрыв на 90% означает начать сначала.
Не делают отправку части идемпотентной. Клиент повторил запрос после таймаута, и часть записалась дважды или сдвинула смещение.
Забывают про незавершённые загрузки. Без срока жизни и чистки хранилище медленно заполняется обрывками.