Search code examples
httpgohandler

How to interrupt an HTTP handler?


Say I have a http handler like this:

func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
        // run code that takes a long time here
        // Executing dd command with cmd.Exec..., etc.

})

Is there a way I can interrupt this function if the user refreshes the page or kills the request some other way without running the subsequent code and how would I do it?

I tried doing this:

notify := r.Context().Done()
go func() {
    <-notify
     println("Client closed the connection")
     s.downloadCleanup()
     return
}()

but the code after whenever I interrupt it still runs anyway.


Solution

  • There's no way to forcibly tear a goroutine down from any code external to that goroutine.

    Hence the only way to actually interrupt processing is to periodically check whether the client is gone (or whether there's another signal to stop processing).

    Basically that would amount to structuring your handler something like this

    func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    
        done := r.Context().Done()
    
        // Check wheteher we're done
    
        // do some small piece of stuff
    
        // check whether we're done
    
        // do another small piece of stuff
    
        // …rinse, repeat
    })
    

    Now a way to check whether there was something written to a channel, but without blocking the operation is to use the "select with default" idiom:

    select {
        case <- done:
            // We're done
        default:
    }
    

    This statemept executes the code in the "// We're done" block if and only if done was written to or was closed (which is the case with contexts), and otherwis the empty block in the default branch is executed.

    So we can refactor that to something like

    func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    
        done := r.Context().Done()
        closed := func () bool {
            select {
                case <- done:
                    return true
                default:
                    return false
            }
        }
    
        if closed() {
            return
        }
    
        // do some small piece of stuff
    
        if closed() {
            return
        }
    
        // do another small piece of stuff
    
        // …rinse, repeat
    })
    

    Stopping an external process started in an HTTP handler

    To address the OP's comment…

    The os/exec.Cmd type has the Process field, which is of type os.Process and that type supports the Kill method which forcibly brings the running process down.

    The only problem is that exec.Cmd.Run blocks until the process exits, so the goroutine which is executing it cannot execute other code, and if exec.Cmd.Run is called in an HTTP handler, there's no way to cancel it.

    How to best handle running a program in such an asynchronous manner heavily depends on how the process itself is organized but I'd roll like this:

    1. In the handler, prepare the process and then start it using exec.Cmd.Start (as opposed to Run).

      Check the error value Start have returned: if it's nil the process has managed to start OK. Otherwise somehow communicate the failure to the client and quit the handler.

      Once the process is known to had started, the exec.Cmd value has some of its fields populated with process-related information; of particular interest is the Process field which is of type os.Process: that type has the Kill method which may be used to forcibly bring the process down.

    2. Start a goroutine and pass it that exec.Cmd value and a channel of some suitable type (see below).

      That goroutine should call Wait on it and once it returns, it should communicate that fact back to the originating goroutine over that channel.

      Exactly what to communicate, is an open question as it depends on whether you want to collect what the process wrote to its standard output and error streams and/or may be some other data related to the process' activity.

      After sending the data, that goroutine exits.

    3. The main goroutine (executing the handler) should just call exec.Cmd.Process.Kill when it detect the handler should terminate.

      Killing the process eventually unblocks the goroutine which is executing Wait on that same exec.Cmd value as the process exits.

      After killing the process, the handler goroutine waits on the channel to hear back from the goroutine watching the process. The handler does something with that data (may be logs it or whatever) and exits.