3

I am trying to implement a global button counter that updates as any/different users click it. So the idea is if one person clicks the button, I see the counter update on my instance of the page.

I currently have the long polling technique working, or so I think, but after review I believe I have an error with "broadcasting" the update to all browsers.

The error currently is that if for example I have two browsers open, and I continuously click on one browser, that browser that I click the button only updates half the time. It will get 1 3 5 etc while the other browser displays 2 4 6 etc.

After reviewing online, I think this may have to do with channels and broadcasting to all those browsers that are on the site. If anyone can help me with an example of how I might send the update to all browsers, every time, I'd really appreciate it.

Client:

<html>
<script language=javascript>

function longpoll(url, callback) {

    var req = new XMLHttpRequest (); 
    req.open ('GET', url, true); 

    req.onreadystatechange = function (aEvt) {
        if (req.readyState == 4) { 
            if (req.status == 200) {
                callback(req.responseText);
                longpoll(url, callback);
            } else {
                alert ("long-poll connection lost");
            }
        }
    };

    req.send(null);
}

function recv(msg) {

    var box = document.getElementById("counter");

    box.innerHTML += msg + "\n";
}
function send() {


    var box = document.getElementById("counter");

  var req = new XMLHttpRequest (); 
    req.open ('POST', "/push?rcpt=", true); 

    req.onreadystatechange = function (aEvt) {
        if (req.readyState == 4) { 
            if (req.status == 200) {
            } else {
                alert ("failed to send!");
            }
        }
  };
  req.send("hi")

  //box.innerHTML += "test" ;  
}
</script>
<body onload="longpoll('/poll', recv);">

<h1> Long-Poll Chat Demo </h1>

<p id="counter"></p>
<button onclick="send()" id="test">Test Button</button>
</body>
</html>

Server:

package main

import (
    "net/http"
    "log"
    "io"
//  "io/ioutil"
  "strconv"
)

var messages chan string = make(chan string, 100)

var counter = 0

func PushHandler(w http.ResponseWriter, req *http.Request) {

    //body, err := ioutil.ReadAll(req.Body)

    /*if err != nil {
        w.WriteHeader(400)
    }*/
    counter += 1
    messages <- strconv.Itoa(counter)
}


func PollResponse(w http.ResponseWriter, req *http.Request) {

    io.WriteString(w, <-messages)
}

func main() {
    http.Handle("/", http.FileServer(http.Dir("./")))
    http.HandleFunc("/poll", PollResponse)
    http.HandleFunc("/push", PushHandler)
    err := http.ListenAndServe(":8010", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}
ZAX
  • 968
  • 3
  • 21
  • 49
  • can you add some debug output and see when the push requests are going through? – Brenden Nov 06 '13 at 01:18
  • @Brenden if you give me more instruction on what you'd like, I'd be happy to oblige. The push requests go through with the `send()` function on the client side, which is activated whenever a user pushes the button – ZAX Nov 06 '13 at 01:28
  • I would add some `fmt.Println()` calls to confirm WHEN those calls are going through as opposed to WHEN the poll responses are going through. I'll run a local test in an hour or so and see what results I can get (I'm at work currently and can't test your code) – Brenden Nov 06 '13 at 01:35
  • @Brenden As far as I can tell they're going through as expected, one after the other. But thank you so much for helping me out! I really appreciate it! I look forward to hearing back from you in a bit! – ZAX Nov 06 '13 at 01:42
  • I got it running locally and it works as expected. The number are sequential. I'm testing on Linux/Chromium, what are you testing with? Can you manually request poll and push URLs without your Javascript? Does it work differently? – Brenden Nov 06 '13 at 02:28
  • @Brendan I have it running in Chrome. Make sure you open two separate tabs running the client. I just checked again, no tweaks since I've posted, and I still have it not updating in both tabs. They alternate – ZAX Nov 06 '13 at 02:33
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/40602/discussion-between-brenden-and-zax) – Brenden Nov 06 '13 at 02:42
  • Out of the topic, maybe you wnat to try HTML5 websocket with http://godoc.org/code.google.com/p/go.net/websocket – nvcnvn Nov 06 '13 at 10:24

2 Answers2

8

Go channels are not multi-cast. That is, if you have multiple goroutines reading from the channel, only one will wake up when you write a value to the channel rather than it being broadcast to all readers.

One alternative would be to use a condition variable instead:

var (
    lock sync.Mutex
    cond = sync.NewCond(&lock)
)

In your PollResponse handler, you can wait on the condition with:

lock.Lock()
cond.Wait()
message := strconv.Itoa(counter)
lock.Unlock()
// write message to response

In your PushHandler handler, you can broadcast the change with:

lock.Lock()
counter += 1
cond.Broadcast()
lock.Unlock()
James Henstridge
  • 42,244
  • 6
  • 132
  • 114
  • terrible question, but I also get frustrated with Go's documentation... which package do those come from. I've tried importing from sync, cond, lock, and then sync/cond, and sync/lock as well. I'm not getting it right though – ZAX Nov 06 '13 at 02:44
  • `sync` is the package name, so `import "sync"` will do. – James Henstridge Nov 06 '13 at 02:55
  • In the PollReponse handler, would I write the message to the resopnse in the same way utlizing `io.WriteString(w, <-messages)` – ZAX Nov 06 '13 at 03:17
  • if I add that line after your comments, but actually io.WriteString(w, message), where message is the variable you utilize in your solution, the problem still persists the same – ZAX Nov 06 '13 at 03:23
  • Really? I just tried modifying your example code with the changes I suggested, and testing it with curl shows that multiple `/poll` requests get woken from a single `/push` request. – James Henstridge Nov 06 '13 at 04:06
  • I have tried... I encourage you to copy paste the code and see what happens with two windows open on the client. Its the same :( – ZAX Nov 06 '13 at 04:10
  • 1
    Here's the version I tried: http://paste.ubuntu.com/6368785/ -- I ran `curl http://localhost:8010/poll` in two terminal windows, and ran `curl http://localhost:8010/push` in a third. The first two windows blocked until I executed the command in the third, at which point they both printed the same counter value. – James Henstridge Nov 06 '13 at 05:58
  • Is there something wrong with my long polling code on the client side then? That's the only thing I can think of but I don't see any glaring issues – ZAX Nov 06 '13 at 06:43
  • @JamesHenstridge I confirm it works in curl, the problem was chrome only allows 1 request at a time!! I proved this by trying localhost and 127.0.0.1 at the same time and I got the correct results (same results as curl) – Brenden Nov 06 '13 at 06:49
  • Perhaps you're running into the browser's limit on simultaneous connections to a single site or a caching issue then – James Henstridge Nov 06 '13 at 07:53
  • I googled the issue and it seems specific to Chrome. I don't believe there is a user configurable setting, it's just an internal mechanism. – Brenden Nov 06 '13 at 16:58
8

The issue isn't the Go code(alone;see PS PS), it's the browser(Chrome). Making 2 requests to the same URL happens in sequence, not in parallel.

Solution You need to add a unique timestamp to the longpoll URL to trick the browser:

req.open ('GET', url+"?"+(new Date().getTime()), true); 

PS - I learned a lot about Go channels and mutex with this question. Thanks :)

PS PS - James' answer (https://stackoverflow.com/a/19803051/143225) is key to getting the server side Go code to handle more than 1 request at a time, because Go channels are blocking which means only 1 goroutine can receive at a time. So the solution to the OP's question is a combination of front and back end code changes.

Community
  • 1
  • 1
Brenden
  • 7,708
  • 11
  • 61
  • 75
  • Cannot explain how much I appreciate you working with me in chat, for several hours, on getting this answer! Everyone up vote! – ZAX Nov 06 '13 at 07:03
  • The Go code in the question was also broken thoug, right? This alone can't solve the problem. – James Henstridge Nov 06 '13 at 07:54
  • @JamesHenstridge I wondered that, but I had already switched to Mutex code you suggested. I'd like to switch back to the initial code to verify this, but I believe your answer was key because channels cannot be read by more than 1 goroutine. – Brenden Nov 06 '13 at 16:46
  • @JamesHenstridge I've updated my answer to give you credit for the Go code changes. Thank you. – Brenden Nov 06 '13 at 16:57