I am working on a Spring-MVC application in which users are able to download files. The users can click on an attachment which triggers a download mechanism.
Yesterday, when multiple downloads and two of which had approximately 2 GB files, it caused an out of memory error(log below).
To avoid this problem, one way to solve this problem seemed like streaming the download data in chunks, and only processing those chunks in Service layer, rather than the entire file.
Unfortunately, I don't know how to move ahead with this, any help would be nice. If this option can't fly, any recommendations on how to solve this problem.
Error log :
HTTP Status 500 - Handler processing failed; nested exception is java.lang.OutOfMemoryError: Direct buffer memory type Exception report message Handler processing failed; nested exception is java.lang.OutOfMemoryError: Direct buffer memory description The server encountered an internal error that prevented it from fulfilling this request. exception org.springframework.web.util.NestedServletException: Handler processing failed; nested exception is java.lang.OutOfMemoryError: Direct buffer memory org.springframework.web.servlet.DispatcherServlet.triggerAfterCompletionWithError(DispatcherServlet.java:1303) org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:977) org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:858) javax.servlet.http.HttpServlet.service(HttpServlet.java:620) org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) Controller code :
@RequestMapping(value = "/download/attachment/{attachid}", method = RequestMethod.GET) public void getAttachmentFromDatabase(@PathVariable("attachid") int attachid, , HttpServletResponse response,) { response.setContentType("application/octet-stream"); GroupAttachments groupAttachments = this.groupAttachmentsService.getAttachmenById(attachid); response.setHeader("Content-Disposition", "attachment; filename=\"" + groupAttachments.getFileName() + "\""); response.setContentLength(groupAttachments.getSendAttachment().length); FileCopyUtils.copy(groupAttachments.getSendAttachment(), response.getOutputStream()); response.flushBuffer(); } Service layer :
@Override public GroupAttachments getAttachmenById(int attachId) { Person person = this.personService.getCurrentlyAuthenticatedUser(); GroupAttachments groupAttachments = this.groupAttachmentsDAO.getAttachmenById(attachId); GroupMembers groupMembers = this.groupMembersService.returnMembersMatchingUsernameAccountId(person.getUsername(), groupAttachments.getGroupId()); if (!(groupMembers == null)) { if (person.getUsername().equals(groupMembers.getMemberUsername())) { try { Path path = Paths.get(msg + groupAttachments.getGroupId() + "/" + groupAttachments.getFileIdentifier()); groupAttachments.setSendAttachment(Files.readAllBytes(path)); return groupAttachments; } catch (IOException ignored) { this.groupAttachmentsDAO.removeAttachment(attachId); return null; } } return null; } else { return null; } } Thank you. :-)
Update
New Download mechanism :
Controller :
public ResponseEntity<byte[]> getAttachmentFromDatabase(@PathVariable("attachid") int attachid, @PathVariable("groupaccountid") Long groupAccountId, @PathVariable("api") String api, HttpServletResponse response, @PathVariable("type") boolean type) { Path path = this.groupAttachmentsService.getAttachmentPathById(attachid); GroupAttachments groupAttachments = this.groupAttachmentsService.getAttachmentObjectOnlyById(attachid); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=\""+groupAttachments.getFileName()+"\""); try { OutputStream outputStream = response.getOutputStream(); Files.copy(path,outputStream); outputStream.flush(); outputStream.close(); response.flushBuffer(); } Service layer :
@Override public Path getAttachmentPathById(int attachId){ Person person = this.personService.getCurrentlyAuthenticatedUser(); GroupAttachments groupAttachments = this.groupAttachmentsDAO.getAttachmenById(attachId); GroupMembers groupMembers = this.groupMembersService.returnMembersMatchingUsernameAccountId(person.getUsername(), groupAttachments.getGroupId()); if (!(groupMembers == null)) { if (person.getUsername().equals(groupMembers.getMemberUsername())) { try { return Paths.get(msg + groupAttachments.getGroupId() + "/" + groupAttachments.getFileIdentifier()); } catch (Exception ignored) { return null; } } return null; } else { return null; } }
Files.readAllBytesloads the whole2Gbinto memory. Simply don't. Delay the loading and instead of first loading it, read it part by part and stream it directly. UseFiles.copy(Path, OutputStream)instead.