Search code examples
spring-bootkotlinfile-ioqr-codescalability

Why does the below file generation method return FileNotFoundException on simultaneous request to this endpoint?


I have written an endpoint to return a zip containing multiple qr with values based on the details in my database. When I did a load test by making multiple requests to this endpoint, It throws FileNotFoundException. But that doesn't happend if the requests are made at a specific time interval.

@GetMapping(value = ["/{sysId}/code"], produces = ["application/zip"])
fun generateQrCode1(@PathVariable sysId: Int): ResponseEntity<InputStreamResource> {
    val sysDetails = sysService.getById(sysId)
    val productDetails = productService.getProductByProductId(sysDetails.productId)
    val zipName = sysDetails.productId.toString() + ".zip"
    FileOutputStream(zipName).use { fileOutputStream ->
        ZipOutputStream(fileOutputStream).use { zipOutputStream ->
            for (i in 1..sysDetails.length) {
                val u = "http://" + "$domain/" + "?sysId=${sysId}&pid=$i"
                val x = generateQRCodeWithText(u, 350, 300, arrayOf("QR CODE TEST"))
                val byteArrayOutputStream = ByteArrayOutputStream()
                byteArrayOutputStream.write(x!!)
                val zipEntry = ZipEntry(
                    "${
                        productDetails.name
                    }-${sysId}-$i.jpg"
                )
                zipOutputStream.putNextEntry(zipEntry)
                byteArrayOutputStream.writeTo(zipOutputStream)
            }
        }
    }
    try {
        return ResponseEntity
            .ok()
            .header("Content-Disposition", "attachment; filename=\"$zipName")
            .body(InputStreamResource(FileInputStream(zipName)))
    } finally {
        java.nio.file.Files.deleteIfExists(Paths.get(zipName))
    }
  
    fun generateQRCodeWithText(data: String?, width: Int?, height: Int, text: Array<String?>): ByteArray? {
    return try {
        val qrCodeWriter = QRCodeWriter()
        val bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, width!!, height)
        val pngOutputStream = ByteArrayOutputStream()
        MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream)
        var pngData = pngOutputStream.toByteArray()

        if (text.size > 0) {
            val totalTextLineToadd = text.size
            val `in`: InputStream = ByteArrayInputStream(pngData)
            val image: BufferedImage = ImageIO.read(`in`)
            val outputImage =
                BufferedImage(image.width, image.height + 25 * totalTextLineToadd, BufferedImage.TYPE_INT_ARGB)
            val g: Graphics = outputImage.graphics
            val g2d = g as Graphics2D
            g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
            g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            g.setColor(Color.WHITE)
            g.fillRect(0, 0, outputImage.width, outputImage.height)
            g.drawImage(image, 0, 0, image.width, image.height, null)
            g.setFont(Font("Arial Black", Font.BOLD, 20))
            val textColor: Color = Color.BLUE
            g.setColor(textColor)
            val fm: FontMetrics = g.getFontMetrics()
            var startingYposition = height + 5
            for (displayText in text) {
                g.drawString(
                    displayText,
                    outputImage.width / 2 - fm.stringWidth(displayText) / 2,
                    startingYposition
                )
                startingYposition += 20
            }
            val baos = ByteArrayOutputStream()
            ImageIO.write(outputImage, "PNG", baos)
            baos.flush()
            pngData = baos.toByteArray()
            baos.close()
        }
        pngData
    } catch (ex: WriterException) {
        throw ex
    } catch (ex: IOException) {
        throw ex
    }
}

Error on multiple requests at same time:

java.io.FileNotFoundException: 2889.zip (No such file or directory)
at java.io.FileInputStream.open0(Native Method) ~[?:1.8.0_292]
 at java.io.FileInputStream.open(FileInputStream.java:195) ~[?:1.8.0_292]
at java.io.FileInputStream.<init>(FileInputStream.java:138) ~[?:1.8.0_292]
 at java.io.FileInputStream.<init>(FileInputStream.java:93) ~[?:1.8.0_292]
 at com.mangoChain.qrApi.QrApi.generateQrCode1(QrApi.kt:136) ~[classes!/:?]

Solution

  • You should make sure your local filename (zipName) is unique, because otherwise multiple threads could delete each other's files.

    You mentioned in the comments that productId can collide, so you could name your files based on sysId instead of (or in addition to) productId. For instance:

    val zipName = "$sysId-${sysDetails.productId}.zip"
    

    Note that you only need to make your local filename unique (zipName), but you can still keep what you want for the user-visible filename in Content-Disposition header:

    return ResponseEntity
        .ok()
        .header("Content-Disposition", "attachment; filename=\"${sysDetails.productId}.zip\"")
        .body(InputStreamResource(FileInputStream(zipName)))
    

    As a side note to this, you should probably use a temporary folder instead of just a plain filename, because currently these files are created in the current workdir.