Search code examples
javascriptjavawebsocketjava-websocket

Cancel a running Java websocket transmission without delay


The problem description is a little longer, so first my question:

When sending binary data over a websocket, how can I immediately cancel the running transmission?

The background:

I have a JavaScript client that works as a GIS (Geographic Information System). It looks and feels similar to, e.g., Google Maps, having a map window that the user can navigate by dragging and zooming with the mouse. If the user, for example, moves the map, the new coordinates are sent to a remote Java process via a websocket. The Java process now creates a new map image and sends it to the client. During image construction, it also sends unfinished intermediate images so the client doesn't have to wait too long. If the client now moves the map several times in quick succession, a new query might arrive at the Java backend while a prior one is still being processed. The prior process will now send outdated images to the client. Therefore, if a query arrives at the Java backend, all processing of prior queries from that client must be aborted and it's results discarted.

I must ensure two things. If a new query arrives,

  • the image generation for older queries must be cancelled.
  • the sending of images from older queries must be cancelled.

Especially the latter one gives me problems. My current solution works like this:

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpSession;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/asyncImage")
public class AsynchronousImageWebSocket {

    private Map<Session, Future<?>> futures = new HashMap<>();

    @OnMessage
    public void onMessage(String message, Session session) throws Exception {
        
        // Start the image generating process, 
        // passing the current websocket session as client ID.
        startImageGeneratingProcess(session);
    }

    // Called by the image generating process to send intermediate or final images to 
    // the client.
    // It also passes the websocket session that has been passed to startImageGeneratingProcess.
    public void sendImage(BufferedImage img, Session session) {
        if (futures.containsKey(session)) {
            if (!futures.get(session).isDone()) {
                
                // If a Future is currently stored for that session and it's not done yet,
                // this means that there already is a running image generating process from
                // a previous query. Cancel it.
                futures.get(session).cancel(true);
                logger.info("SEND cancelled");
            }
            futures.remove(session);
        }
        try (ByteArrayOutputStream out = new ByteArrayOutputStream();) {
            ImageIO.write(img, "PNG", out);

            // Send the image, store the returned Future and associate it with the 
            // client's session.
            Future<?> f = session.getAsyncRemote().sendBinary(ByteBuffer.wrap(out.toByteArray()));
            futures.put(session, f);
        } catch (IOException e) {
            logger.error(e);
        } catch (Exception e) {
            logger.error(e);
        }
    }
}

Unfortunately, the Future's cancel method doesn't seem to be evaluated. A running sendBinary method finishes, even if I call cancel on it's Future. Is there a way to immediately cancel the running sendBinary method of an older processing job?

Thanks for your input and let me know if you need anything else.

p.s. another idea would be to simply keep sending everything and somehow let the client identify and sort out deprecated images. However, generating and sending deprecated images costs a lot of resources which I'd like to save.


Solution

  • Sorry, that was completely my bad. The websocket wasn't blocking. It was synchronization at a different place that caused the sends to execute synchronously.

    If you execute the sendImage method I described above without any synchronization code, it should execute without blocking. Instead, if session.getAsyncRemote().sendBinary is concurrently executed by two threads at the same time, they will both throw an IllegalStateException and abort the sending. In this case, one can catch that exception, discard it and simply resend the last image.

    Based on this, i have altered my sendImage method in the following way:

    public void temporaryImageReady(BufferedImage img, Session session) {
        if (futures.containsKey(session)) {
            if (!futures.get(session).isDone()) {
                futures.get(session).cancel(true);
                logger.info("SEND cancelled");
            }
            futures.remove(session);
        }
    
        try {
            send(img, handle);
        } catch (IOException e) {
            logger.error(e);
        } catch (IllegalStateException e) {
            logger.info("Image send collision, resend last image.");
            try {
                send(img, handle);
            } catch (Exception e1) {
                logger.info("Image resend after collision failed.");
                logger.error(e);
            }
        } catch (Exception e) {
            logger.error(e);
        }
    }
        
    private void send(BufferedImage img) throws Exception {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream();) {
            ImageIO.write(img, "PNG", out);
            Future<?> f = session.getAsyncRemote().sendBinary(ByteBuffer.wrap(out.toByteArray()));
            futures.put(session, f);
        }
    }