Repository: bokeh/bokeh

Issue:
Data not always bytes
I occasionally get the following error when running bokeh serve on the following script
### Error

``` python
ERROR:tornado.application:Exception in callback <bound method BokehTornado.keep_alive of <bokeh.server.tornado.BokehTornado object at 0x7f9d767ef9e8>>
Traceback (most recent call last):
  File "/home/mrocklin/Software/anaconda/lib/python3.4/site-packages/tornado/ioloop.py", line 1039, in _run
    return self.callback()
  File "/home/mrocklin/Software/anaconda/lib/python3.4/site-packages/bokeh/server/tornado.py", line 182, in keep_alive
    c.send_ping()
  File "/home/mrocklin/Software/anaconda/lib/python3.4/site-packages/bokeh/server/connection.py", line 44, in send_ping
    self._socket.ping(str(self._ping_count))
  File "/home/mrocklin/Software/anaconda/lib/python3.4/site-packages/tornado/websocket.py", line 266, in ping
    self.ws_connection.write_ping(data)
  File "/home/mrocklin/Software/anaconda/lib/python3.4/site-packages/tornado/websocket.py", line 697, in write_ping
    assert isinstance(data, bytes)
```
### Script

``` python
import numpy as np
from datetime import timedelta, datetime

def update_data(*args, **kwargs):
    td = timedelta(milliseconds=100)
    now = datetime.now()

    source.data = {'time': [now - td * i for i in range(100)],
                   'cpu': np.random.random(100).cumsum(),
                   'memory': np.random.random(100).cumsum()}

from toolz import first
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Range1d

source = ColumnDataSource({})
update_data()

p = figure(x_axis_type='datetime', y_range=Range1d(0, 100))
p.line(x='time', y='cpu', source=source)
p.line(x='time', y='memory', source=source)

from bokeh.io import curdoc
curdoc().add(p)
curdoc().add_periodic_callback(update_data, 100)
```

```
$ bokeh serve script.py
```



Discussion:
~~~What version of python?~~~

ping @havocp 

I should say that this didn't cause a serious issue, just in the logs.

Python 3.4, Tornado 4.3 (also tested on 4.2), Ubuntu 14.04

need an encode there in Python 3 it looks like 


Resolving patch:
diff --git a/bokeh/server/connection.py b/bokeh/server/connection.py
--- a/bokeh/server/connection.py
+++ b/bokeh/server/connection.py
@@ -3,6 +3,8 @@
 '''
 from __future__ import absolute_import
 
+import codecs
+
 class ServerConnection(object):
     ''' Wraps a websocket connection to a client.
     '''
@@ -41,7 +43,7 @@ def send_patch_document(self, event):
         return self._socket.send_message(msg)
 
     def send_ping(self):
-        self._socket.ping(str(self._ping_count))
+        self._socket.ping(codecs.encode(str(self._ping_count), "utf-8"))
         self._ping_count += 1
 
     @property
diff --git a/bokeh/server/views/ws.py b/bokeh/server/views/ws.py
--- a/bokeh/server/views/ws.py
+++ b/bokeh/server/views/ws.py
@@ -8,7 +8,7 @@
 
 import random
 import time
-
+import codecs
 
 from tornado import gen
 from tornado.websocket import WebSocketHandler, WebSocketClosedError
@@ -36,6 +36,7 @@ def __init__(self, tornado_app, *args, **kw):
         self.handler = None
         self.connection = None
         self.application_context = kw['application_context']
+        self.latest_pong = -1
         # Note: tornado_app is stored as self.application
         super(WSHandler, self).__init__(tornado_app, *args, **kw)
 
@@ -124,8 +125,14 @@ def on_message(self, fragment):
         raise gen.Return(None)
 
     def on_pong(self, data):
-        #log.debug("received a pong: %r", data)
-        pass
+        # if we get an invalid integer or utf-8 back, either we
+        # sent a buggy ping or the client is evil/broken.
+        try:
+            self.latest_pong = int(codecs.decode(data, 'utf-8'))
+        except UnicodeDecodeError as e:
+            log.error("received invalid unicode in pong %r", data, exc_info=True)
+        except ValueError as e:
+            log.error("received invalid integer in pong %r", data, exc_info=True)
 
     @gen.coroutine
     def send_message(self, message):


Test patch:
diff --git a/bokeh/tests/test_client_server.py b/bokeh/tests/test_client_server.py
--- a/bokeh/tests/test_client_server.py
+++ b/bokeh/tests/test_client_server.py
@@ -29,10 +29,11 @@ def ws_url(server):
 # lets us use a current IOLoop with "with"
 # and ensures the server unlistens
 class ManagedServerLoop(object):
-    def __init__(self, application):
+    def __init__(self, application, **server_kwargs):
         loop = IOLoop()
         loop.make_current()
-        self._server = Server(application, io_loop=loop)
+        server_kwargs['io_loop'] = loop
+        self._server = Server(application, **server_kwargs)
     def __exit__(self, type, value, traceback):
         self._server.unlisten()
         self._server.io_loop.close()
@@ -151,6 +152,31 @@ def test_request_server_info(self):
             session.loop_until_closed()
             assert not session.connected
 
+    def test_ping(self):
+        application = Application()
+        with ManagedServerLoop(application, keep_alive_milliseconds=0) as server:
+            session = ClientSession(url=ws_url(server), io_loop=server.io_loop)
+            session.connect()
+            assert session.connected
+            assert session.document is None
+
+            connection = next(iter(server._tornado._clients))
+            expected_pong = connection._ping_count
+            server._tornado.keep_alive() # send ping
+            session.force_roundtrip()
+
+            self.assertEqual(expected_pong, connection._socket.latest_pong)
+
+            # check that each ping increments by 1
+            server._tornado.keep_alive()
+            session.force_roundtrip()
+
+            self.assertEqual(expected_pong + 1, connection._socket.latest_pong)
+
+            session.close()
+            session.loop_until_closed()
+            assert not session.connected
+
     def test_client_changes_go_to_server(self):
         application = Application()
         with ManagedServerLoop(application) as server: