dragon66 / icafe

Java library for reading, writing, converting and manipulating images and metadata
Eclipse Public License 1.0
204 stars 58 forks source link

ArrayIndexOutOfBoundsException when writing out tiffs across multiple threads #29

Closed reecefenwick closed 8 years ago

reecefenwick commented 8 years ago

I'm currently using this library (1.1-SNAPSHOT) to:

I am running this in a spring-boot application and exposing the above functionality as a web service.

I've just encountered an issue, that I realise has been here all along - just unbeknown to me.

The issue occurs when concurrent requests (on separate threads) come through where both threads are writing out a tiff at the same time.

The scenario :

java.lang.ArrayIndexOutOfBoundsException: 10104
    at com.icafe4j.image.compression.packbits.Packbits.packbits(Unknown Source)
    at com.icafe4j.image.writer.TIFFWriter.compressSample(Unknown Source)
    at com.icafe4j.image.writer.TIFFWriter.writeTrueColor(Unknown Source)
    at com.icafe4j.image.writer.TIFFWriter.writePageData(Unknown Source)
    at com.icafe4j.image.writer.TIFFWriter.writePage(Unknown Source)
    at com.icafe4j.image.tiff.TIFFTweaker.writePage(Unknown Source)
    at au.com.reecefenwick.imaging.rest.ImageController.manipulateImage(ImageController.java:147)
    at au.com.reecefenwick.imaging.rest.ImageController.drawOnImage(ImageController.java:99)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:817)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:731)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:968)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:870)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:844)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
    at org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration$ApplicationContextHeaderFilter.doFilterInternal(EndpointWebMvcAutoConfiguration.java:237)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

    ...

java.lang.ArrayIndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException

This is my code where I am reading an image from a HTTP stream, modifying the BufferedImage and writing out as a TIFF to the HTTP response.

Don't read too much into the "CustomTiffTweaker", I created that after I discovered this issue to override the behaviour of a particular method, more on that below.

private void manipulateImage(InputStream tiffInputStream,
                             HttpServletResponse response, String contentType) throws IOException {
        ImageReader reader = splitMultiPageImage(tiffInputStream);
        int totalPages = reader.getNumImages(true);

        // Prepare tiff writer
        TIFFWriter writer = new TIFFWriter();
        writer.setImageParam(buildTiffOptions()[0]);
        List<IFD> ifds = new ArrayList<IFD>();
        RandomAccessOutputStream rout = new MemoryCacheRandomAccessOutputStream(response.getOutputStream());
        int writeOffset = CustomTiffTweaker.prepareForWrite(rout);

        String ext = ".tif";
        response.setHeader("Content-Disposition", "attachment; filename=" + UUID.randomUUID() + ext);
        response.setContentType(contentType);

        for (int i = 0; i < totalPages; i++ ) {
            BufferedImage bufferedImage = reader.read(i);
            bufferedImage = imageManipulationService.drawOnImage(bufferedImage);
            writeOffset = CustomTiffTweaker.writePage(bufferedImage, rout, ifds, writeOffset, writer);
            bufferedImage = null;
        }

        CustomTiffTweaker.finishWrite(rout, ifds);
        rout.close();
        response.flushBuffer();
    }

The exception is being thrown here in this class

My attempt to debug/workaround

I created CustomTiffTweaker.java as referenced above, extending the TIFFTweaker - overriding the writePage() method and implementing a retry mechanism.

public class CustomTiffTweaker extends TIFFTweaker {

    private final Logger log = LoggerFactory.getLogger(CustomTiffTweaker.class);

    public static int writePage(BufferedImage image, RandomAccessOutputStream rout, List<IFD> ifds,
                                int writeOffset, TIFFWriter writer) throws IOException {
        int count = 0;
        int maxTries = 15;
        while(true) {
            try {
                // break out of loop, or return, on success
                writeOffset = writer.writePage(image, 0, 0, rout, writeOffset);

                ifds.add(writer.getIFD());
                return writeOffset;
            } catch (Exception e) {
                // handle exception
                System.out.println("Retrying");
                if (++count == maxTries) throw new RuntimeException("");
            }
        }
    }
}

I managed to stop the issue from happening by doing the above, which is obviously very dodgy :)

What really sucks about this issue is that is being silently consumed, with a stacktrace going to stdout.

Has anyone had similar experiences when using this library in a multi-threaded environment?

dragon66 commented 8 years ago

@reecefenwick : one of the things about icafe is that it is never meant to be used in a multithreading environment without proper control. That said, I will take a look at the issue when time allowed but don't expect too much from it.

In the mean time, you can try a different compression method than Packbits which I took from some other source instead of coding it myself.

By the way, with Java's build-in concurrency mechanism, you can safely run single threaded code in a multi-threaded environment. The way you are trying to avoid the issue is not recommended.

huffstler commented 8 years ago

I would like to leave a comment for anyone else who might find this while searching for a fix to their problems (like I did). The built in concurrency mechanism that dragon66 is talking about is the synchronize() block.

Info about it can be found here.

A short tl;dr

You can synchronize entire methods by doing this: public (static) synchronize void methodCall() { . . .

or you can synchronize smaller blocks of code by doing this:

synchronize(this) {
 ... code goes here
}
dragon66 commented 8 years ago

@huffstler Thanks a lot for coming back to add information which will benefit other users!