Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net/http: add CloseNotifier #2510

Closed
gopherbot opened this issue Nov 30, 2011 · 32 comments
Closed

net/http: add CloseNotifier #2510

gopherbot opened this issue Nov 30, 2011 · 32 comments
Milestone

Comments

@gopherbot
Copy link

by blake.mizerany:

Per email from Brad:

It looks like what you don't actually care about owning the TCP connection and ideally
just want something like:

 var clientClosed chan bool
 cn, ok := w.(http.CloseNotifier)
 if ok {
     clientClosed = cn.CloseChannel() // returns a chan bool, written to when/if this http request is closed
 }

 select {
  case <-clientClosed:
     // cancel expensive stuff
  case ....
  }

https://groups.google.com/d/topic/golang-nuts/PcEvewBB-AQ/discussion
@bradfitz
Copy link
Contributor

Comment 1:

Russ, any objections?  I've now seen this request several times in different forms,
including from Blake again just now.
It'd be an optional interface, like Hijacker.

Owner changed to @bradfitz.

Status changed to Accepted.

@rsc
Copy link
Contributor

rsc commented Nov 30, 2011

Comment 2:

doesn't seem like we want to encourage people to depend on it,
but i guess it's fine.

@bradfitz
Copy link
Contributor

bradfitz commented Dec 6, 2011

Comment 3:

So, I implemented this, but now I'm not sure how I feel about it.  I thought it'd be
prettier.
http://golang.org/cl/5453063
I think this could all be implemented outside of the http package (by providing an
http.Server with a wrapper Listener which returns wrapped Conns which do this magic).  I
think.
But then we could keep the ugliness and complexity out the core.

Status changed to Thinking.

@gopherbot
Copy link
Author

Comment 4 by blake.mizerany:

I'm trying to wrap my head around wrapper that returns wrappers.

@bradfitz
Copy link
Contributor

bradfitz commented Dec 6, 2011

Comment 5:

Think of it as a net.Listener than returns net.Conns then, deferring to a tcp listener
and tcp conn for most the work, except for bookkeeping which connections are being
handled by which handlers.
Then you'd need a new Handler wrapper that works with your wrapped connections.  As
inspiration, see http://golang.org/pkg/http/#TimeoutHandler

@rsc
Copy link
Contributor

rsc commented Dec 6, 2011

Comment 6:

this is straying pretty far from http.
why not just hijack the connection?

@rsc
Copy link
Contributor

rsc commented Dec 6, 2011

Comment 7:

Or just Hijack the connection.

@gopherbot
Copy link
Author

Comment 8 by blake.mizerany:

I'm not against that, at all. The problem, well, inconvenience, comes
when I can no longer take advantage of all the work the underlying
object of http.ResponseWriter was doing because it no longer allows
writes after a hijacking.  I find myself copy and pasting <ducks> code
from Go's src (i.e. WriteHeader).
One way around this is to factor out the convenience methods into a public API.

@rsc
Copy link
Contributor

rsc commented Dec 7, 2011

Comment 9:

But a public API for what?  You're really not speaking HTTP anymore,
and it is unclear that these kinds of helpers for this non-HTTP protocol
belong in the HTTP library.  As soon as there is a proxy in the middle
the semantics of your server break badly.
Since the client has to know about this magic anyway, it seems
cleaner to hijack the connection and switch to a simpler custom
protocol.
Russ

@gopherbot
Copy link
Author

Comment 10 by blake.mizerany:

I'm still speaking HTTP. It's knowing when the connection drops that
is important when doing expensive things. I need to cancel those
expensive things if the connection drops.
That doesn't mean I'm no longer speaking HTTP. It's that in a lot of
cases, I don't know what the proper status code is until the expensive
work has completed in some way.
For example:
     go func() {
        errCh <- somethingLongRunning(closedCh)
     }()
     select {
     case err := <-errCh:
       if err != nil {
         http.Error(w, err.String(), 500);
       }
     }
If I have to Hijack, I can no longer use anything convenient in the
http package, even though I'm still speaking HTTP, because they all
take the http.ResponseWriter, or are on it; and that is now rejecting
all calls to any method.
-b

@gopherbot
Copy link
Author

Comment 11 by blake.mizerany:

Just realized I don't need that select, like I did when I first wrote
the example; but you get the picture.

@rsc
Copy link
Contributor

rsc commented Dec 9, 2011

Comment 12:

Labels changed: added priority-later.

@bradfitz
Copy link
Contributor

Comment 13:

I forgot to mention it earlier, but while implementing this, I realized it's very much
related to implementing HTTP/1.1 pipelining.   In both cases we need to listen for
writes from the client while the ServeHTTP function is running.  Currently we run
ServeHTTP in the same goroutine, which has this comment before it:
                // HTTP cannot have multiple simultaneous active requests.[*]
                // Until the server replies to this request, it can't read another,
                // so we might as well run the handler in this goroutine.
                // [*] Not strictly true: HTTP pipelining.  We could let them all process
                // in parallel even if their responses need to be serialized.
                handler.ServeHTTP(w, w.req)
I'd like to also support pipelining at some point (though SPDY is better).
I do agree with Blake, though, that he's not requesting something crazy or non-HTTP-ish.
 We do tell people often that the reason you can't kill goroutines is because you're
supposed to send an exit message on a channel to a goroutine instead, telling it to
stop.  That's what this bug is about:  letting the HTTP server tell a ServeHTTP
goroutine that if it's doing something expensive that it can stop.  It's not about
whether it's streaming it or what output format it's sending, or whether the request
will ever end.

@rsc
Copy link
Contributor

rsc commented Dec 14, 2011

Comment 14:

I am still skeptical.  If you depend on knowing when the
connection drops, then a proxy in the middle will change
the behavior, since it presumably won't drop the connection.
Russ

@bradfitz
Copy link
Contributor

Comment 15:

It's not for correctness.  It's an optimization to stop doing something expensive if
possible.

@rsc
Copy link
Contributor

rsc commented Dec 14, 2011

Comment 16:

I think this would make three magic interfaces that an
http.ResponseWriter might implement.  Is it plausible
that we'll stop at three?

@bradfitz
Copy link
Contributor

Comment 17:

I didn't know that was a concern.
Would something like:
func (s *Server) DisconnectChannel(r *Request) (CloseNotifier, error)
... be a less offensive API?
My main concern was overhead for the people who don't use this.  My current patch above
isn't good in that regard, but I think it can go down to ~0 with a small amount of
effort.

@rsc
Copy link
Contributor

rsc commented Dec 14, 2011

Comment 18:

The *Server method is nice but probably difficult
in some situations: very little code has the *Server
lying around.
CloseNotifier is fine.  s/CloseChannel/Closed/

@gopherbot
Copy link
Author

Comment 20 by blake.mizerany:

Has anymore thought been put into this?

@lukescott
Copy link

Comment 21:

A big use case for this is long polling. The fact than IE 9 doesn't support websockets
pretty much means I have to do long polling. Flash is not an option for us.
With long polling I have a select{} on a write channel. The request is hung there until
something comes in on the write channel. I need some way of knowing when an unexpected
disconnect happens. Polling writes is very inefficient.
Hijacking is really wonky. After you Hijack there's no way to Un-Hijack it. I understand
the purpose of the Hijack - start off as HTTP protocol and do something else (websocket
handshake, typically). But hiding the connection object from the public API is just
crude. Right now the only way to make this work is either to modify the core, or Hijack
the connection and duplicate a bunch of code that ResponseWriter is supposed to do. 
What it comes down to is I need the connection/buffer objects AND I want to continue
speaking HTTP. There should be no reason I shouldn't be able to do that. 
If you really don't want to expose the connection object a function that blocks until
the connection closes OR something is written by ResponseWriter would work. In that case
I can block on my write channel on another goroutine. I don't necessarily need a
callback, I just need to bock the Handler from finishing until a disconnect/write occurs.

@lukescott
Copy link

Comment 22:

Looking at the proposed patch, it looks like the code is run on every request whether
you need it or not. How about something like this:
type HandleBlocker interface {
    BlockHandler(release chan bool)
}
func (r *response) BlockHandler(release chan bool) {
    peeked := make(chan bool)
    go func() {
        bs, _ := c.buf.Reader.Peek(1)
        
        if len(bs) == 0 {
            peeked <- true
        }
    }()
    select {
    case <-peeked:
    case <-release:
    }
}
With that I should be able to do something like this:
func lpHandler(w http.ResponseWriter, req *http.Request) {
    //... write chan string, read chan string, etc...
    release := make(chan bool)
    go func() {
        m := <-write
        io.WriteString(w, m)
        for {
            select {
            case m := write
                io.WriteString(w, m)
            default
                return
            }
        }
        release <- true
    }()
    w.(HandleBlocker).BlockHandler(release)
}
What do you think?

@bradfitz
Copy link
Contributor

bradfitz commented Jul 9, 2012

Comment 23:

[+sameer, since this is related to his "stop" cancellation work]

@rsc
Copy link
Contributor

rsc commented Sep 12, 2012

Comment 24:

Labels changed: added go1.1maybe.

@lukescott
Copy link

Comment 25:

The code I suggested didn't work as well as I thought. I had an issue where readRequest
is called after Peek returns causing readRequest to hang.
What I would really like to see is HTTP pipelining instead. A disconnect notification
can be sent when the next readRequest fails.
It would also be beneficial to break server.go up into more manageable chunks. Perhaps
it should be its own package. There also seems to be some slight duplication between
server.go and the rest of the net/http library. I started to rewrite it a bit, but my
lack of understanding of 100 continue headers makes me a bit nervous.

@gopherbot
Copy link
Author

Comment 26 by lawrence.kesteloot:

For two years I've been using the Tornado web framework (Python), which is
single-threaded and asynchronous. It has an on_connection_close() callback that is
called immediately when the client disconnects (modulo proxies etc). It's incredibly
useful. Here are two examples:
A chat room where you GET for the next line from other chatters. When a client
disconnects you want to announce to the room that the person has left.
A GET request to retrieve some data. The data also comes back with a version number. The
client then immediately does another GET, specifying the version number. The GET hangs
as long as the specified version number is current. The GET is released with new data
and version when they change. You may have lots of clients waiting like this and you
want to clean out blocked handlers when clients disconnect. It may also be important to
know when they disconnect (say, to garbage-collect data structures associated with them).
When I started learning Go this was almost the first thing I looked for (along with how
many long-running handlers it could handle). I agree with others that hijacking is not
the right solution.
Several of the projects I've implemented in the last two years would not be possible (I
believe) in Go.

@bradfitz
Copy link
Contributor

Comment 27:

It is still my intention to add this.
I just want to do it cleanly, and without impacting performance in the common case.
This does not enable anything that's not already possible.  It just makes things more
efficient (better connection re-use) and convenient.

@bradfitz
Copy link
Contributor

Comment 28:

Update on this via issue #4403, comment #3:
https://golang.org/issue/4403?c=3

@bradfitz
Copy link
Contributor

Comment 29:

Update on this via issue #4403, comment #3:
https://golang.org/issue/4403?c=3

@gopherbot
Copy link
Author

Comment 30 by blake.mizerany:

Boom.
On Tuesday, November 20, 2012, wrote:

@bradfitz
Copy link
Contributor

bradfitz commented Dec 3, 2012

Comment 31:

CL out for review at https://golang.org/cl/6867050/

Status changed to Started.

@bradfitz
Copy link
Contributor

bradfitz commented Dec 6, 2012

Comment 32:

This issue was closed by revision 4fb78c3.

Status changed to Fixed.

@gopherbot
Copy link
Author

Comment 33 by blake.mizerany:

Thank you.
On Wednesday, December 5, 2012, wrote:

@rsc rsc added this to the Go1.1 milestone Apr 14, 2015
@rsc rsc removed the go1.1maybe label Apr 14, 2015
@golang golang locked and limited conversation to collaborators Jun 24, 2016
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants