Possible race condition when replacing Figure object

Hi all,

I’m attempting to use the bokeh server to present a bokeh application with the following workflow:

  1. display a graph,

  2. display controls,

  3. when the user updates a control, trigger an event in the application:

a) run a query and do some analytics to build a new graph,

b) push the new graph to the client through the bokeh server.

The application works great for the most part, but if I change multiple controls too quickly (i.e. if I change a control in the client while the application is still building the graph for an earlier control change) then the application and client get into an infinite loop where they send PATCH-DOC messages back and forth, writing and overwriting the contents of the graph each time. I’ve written a small test case:

import time
import numpy
import datetime
import bokeh.client
import bokeh.plotting
import bokeh.models.widgets

n = 3
m = 5

numpy.random.seed(1)

data = numpy.cumsum(numpy.random.randn(n, m), axis = 1)

plot = bokeh.plotting.Figure()
plotWrapper = bokeh.models.widgets.HBox(plot)
buttons = bokeh.models.widgets.CheckboxButtonGroup(labels = map(str, range(n)), active = [0])

class Updater(object):

def __init__(self):
    self.__count = 0
       
def __call__(self, *args, **kwargs):
    print datetime.datetime.now(), "Starting update", self.__count, args, kwargs
    plot = bokeh.plotting.Figure()
    for index in buttons.active:
        print datetime.datetime.now(), "Adding", index
        plot.line(range(m), data[index])
    print datetime.datetime.now(), "Sleeping"
    time.sleep(5)
    plotWrapper.children = [plot]
    print datetime.datetime.now(), "Done with update", self.__count
    self.__count += 1

update = Updater()
update()
buttons.on_click(update)

controls = bokeh.models.widgets.VBoxForm(buttons)
app = bokeh.models.widgets.VBox(controls, plotWrapper)
doc = bokeh.plotting.curdoc()
doc.add_root(app)

session = bokeh.client.push_session(doc)
print session.id
session.loop_until_closed()

``

If you start the bokeh server and run that script on the same machine, and then open a tab to http://localhost:5006/?bokeh-session-id= with the session ID printed by the application, you will see three checkbox buttons (with one active), and a graph with one line. If you quickly click the other two buttons, five seconds later a second line will show up, and then five seconds after that the client will start experiencing problems: it may temporarily show all three lines, and then flash back and forth between the two lines and the three lines, or it may just hang, taking up a full core.

The application will write output similar to the following, and will continue to use significant cpu:

2016-01-21 20:24:07.586236 Starting update 0 () {}
2016-01-21 20:24:07.590865 Adding 0
2016-01-21 20:24:07.593379 Sleeping
2016-01-21 20:24:12.598626 Done with update 0

2016-01-21 20:24:45.647457 Starting update 1 ([0, 1],) {}
2016-01-21 20:24:45.651199 Adding 0
2016-01-21 20:24:45.652940 Adding 1
2016-01-21 20:24:45.654289 Sleeping
2016-01-21 20:24:50.663665 Done with update 1
2016-01-21 20:24:50.664638 Starting update 2 ([0, 1, 2],) {}
2016-01-21 20:24:50.668122 Adding 0
2016-01-21 20:24:50.669778 Adding 1
2016-01-21 20:24:50.671121 Adding 2
2016-01-21 20:24:50.672656 Sleeping
2016-01-21 20:24:55.682917 Done with update 2

``

The bokeh server will also use significant CPU.

If I do a tcpdump, I see the PATCH-DOC json messages going back and forth between both the client and the server, and between the application and the server. On some runs, I will see runtime errors in the server log:

ERROR:bokeh.server.protocol.server_handler:error handling message Message ‘PATCH-DOC’ (revision 1): RuntimeError(‘Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document’,)
DEBUG:bokeh.server.protocol.server_handler: message header {u’msgid’: u’C1299A1D93DD4D7483B1893454CF55AF’, u’msgtype’: u’PATCH-DOC’} content {u’references’: [{u’attributes’: {u’name’: u’title_panel’, u’tags’: }, u’type’: u’LayoutBox’, u’id’: u’LayoutBox-7F186730CFC743078DB472E0DD22F4AB’}], u’events’: [{u’new’: [{u’type’: u’LayoutBox’, u’id’: u’LayoutBox-7F186730CFC743078DB472E0DD22F4AB’}], u’kind’: u’ModelChanged’, u’model’: {u’subtype’: u’Figure’, u’type’: u’Plot’, u’id’: u’9bf6a048-035f-4d05-8168-3dd9dfd5e889’}, u’attr’: u’above’}]}
Traceback (most recent call last):
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/protocol/server_handler.py”, line 38, in handle
work = yield handler(message, connection)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/gen.py”, line 1008, in run
value = future.result()
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/concurrent.py”, line 232, in result
raise_exc_info(self._exc_info)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/gen.py”, line 1017, in run
yielded = self.gen.send(value)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/session.py”, line 42, in _needs_document_lock_wrapper
result = yield yield_for_all_futures(func(self, *args, **kwargs))
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/session.py”, line 212, in _handle_patch
message.apply_to_document(self.document)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/protocol/messages/patch_doc.py”, line 80, in apply_to_document
doc.apply_json_patch(self.content)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/document.py”, line 837, in apply_json_patch
raise RuntimeError(“Cannot apply patch to %s which is not in the document” % (str(patched_id)))
RuntimeError: Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document

``

And console errors in the client:

Bokeh: Unhandled ERROR reply to C1299A1D93DD4D7483B1893454CF55AF: RuntimeError(‘Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document’,)

``

Does this seem like a bug, or am I misusing functionality?

I suspect that I could alleviate this issue to some degree if I re-wrote my update function to do the graph calculation another thread, or to make use of co-routines, but it’s clear to me how best to do this. Any ideas?

Thanks,

Peter

Hi,

This is a bug I think (similar to / the same as https://github.com/bokeh/bokeh/issues/3679 ?)

It’s hard to speculate on how to work around it without debugging why it happens. It looks complicated enough that someone will really have to schedule time to dig in on it, though. Step 1 is a test case so thanks for that!

It may be that we need to be quieter about events on no-longer-in-the-document models, because if a model is removed there could always be more events queued up for it, and the right thing to do would be to ignore them. But, I wouldn’t just turn off this error without first understanding the sequence of events here and be sure that’s what’s happening, and ideally we’d do something like specifically track removed model ids for a period of time so we could silence the error only for removed ids, not for out-of-the-blue ids that we really shouldn’t have been sent an event for at all.

The only way this would be an application bug is if two pieces of code are modifying the same model’s property at the same time; right now bokeh server really can’t deal with that in a sensible way (and it isn’t clear what a sensible way would be). However here I think only your client script is modifying the children of the plotWrapper, so that should be fine. Maybe it’s as simple as a race where the browser client is still sending changes for a plot that’s been removed elsewhere, and so when those changes arrive they should be ignored.

Havoc

···

On Thu, Jan 21, 2016 at 3:45 PM, [email protected] wrote:

Hi all,

I’m attempting to use the bokeh server to present a bokeh application with the following workflow:

  1. display a graph,
  1. display controls,
  1. when the user updates a control, trigger an event in the application:

a) run a query and do some analytics to build a new graph,

b) push the new graph to the client through the bokeh server.

The application works great for the most part, but if I change multiple controls too quickly (i.e. if I change a control in the client while the application is still building the graph for an earlier control change) then the application and client get into an infinite loop where they send PATCH-DOC messages back and forth, writing and overwriting the contents of the graph each time. I’ve written a small test case:

import time
import numpy
import datetime
import bokeh.client
import bokeh.plotting
import bokeh.models.widgets

n = 3
m = 5

numpy.random.seed(1)

data = numpy.cumsum(numpy.random.randn(n, m), axis = 1)

plot = bokeh.plotting.Figure()
plotWrapper = bokeh.models.widgets.HBox(plot)
buttons = bokeh.models.widgets.CheckboxButtonGroup(labels = map(str, range(n)), active = [0])

class Updater(object):

def __init__(self):
    self.__count = 0
       
def __call__(self, *args, **kwargs):
    print datetime.datetime.now(), "Starting update", self.__count, args, kwargs
    plot = bokeh.plotting.Figure()
    for index in buttons.active:
        print datetime.datetime.now(), "Adding", index
        plot.line(range(m), data[index])
    print datetime.datetime.now(), "Sleeping"
    time.sleep(5)
    plotWrapper.children = [plot]
    print datetime.datetime.now(), "Done with update", self.__count
    self.__count += 1

update = Updater()
update()
buttons.on_click(update)

controls = bokeh.models.widgets.VBoxForm(buttons)
app = bokeh.models.widgets.VBox(controls, plotWrapper)
doc = bokeh.plotting.curdoc()
doc.add_root(app)

session = bokeh.client.push_session(doc)
print session.id
session.loop_until_closed()

``

If you start the bokeh server and run that script on the same machine, and then open a tab to http://localhost:5006/?bokeh-session-id= with the session ID printed by the application, you will see three checkbox buttons (with one active), and a graph with one line. If you quickly click the other two buttons, five seconds later a second line will show up, and then five seconds after that the client will start experiencing problems: it may temporarily show all three lines, and then flash back and forth between the two lines and the three lines, or it may just hang, taking up a full core.

The application will write output similar to the following, and will continue to use significant cpu:

2016-01-21 20:24:07.586236 Starting update 0 () {}
2016-01-21 20:24:07.590865 Adding 0
2016-01-21 20:24:07.593379 Sleeping
2016-01-21 20:24:12.598626 Done with update 0

2016-01-21 20:24:45.647457 Starting update 1 ([0, 1],) {}
2016-01-21 20:24:45.651199 Adding 0
2016-01-21 20:24:45.652940 Adding 1
2016-01-21 20:24:45.654289 Sleeping
2016-01-21 20:24:50.663665 Done with update 1
2016-01-21 20:24:50.664638 Starting update 2 ([0, 1, 2],) {}
2016-01-21 20:24:50.668122 Adding 0
2016-01-21 20:24:50.669778 Adding 1
2016-01-21 20:24:50.671121 Adding 2
2016-01-21 20:24:50.672656 Sleeping
2016-01-21 20:24:55.682917 Done with update 2

``

The bokeh server will also use significant CPU.

If I do a tcpdump, I see the PATCH-DOC json messages going back and forth between both the client and the server, and between the application and the server. On some runs, I will see runtime errors in the server log:

ERROR:bokeh.server.protocol.server_handler:error handling message Message ‘PATCH-DOC’ (revision 1): RuntimeError(‘Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document’,)
DEBUG:bokeh.server.protocol.server_handler: message header {u’msgid’: u’C1299A1D93DD4D7483B1893454CF55AF’, u’msgtype’: u’PATCH-DOC’} content {u’references’: [{u’attributes’: {u’name’: u’title_panel’, u’tags’: }, u’type’: u’LayoutBox’, u’id’: u’LayoutBox-7F186730CFC743078DB472E0DD22F4AB’}], u’events’: [{u’new’: [{u’type’: u’LayoutBox’, u’id’: u’LayoutBox-7F186730CFC743078DB472E0DD22F4AB’}], u’kind’: u’ModelChanged’, u’model’: {u’subtype’: u’Figure’, u’type’: u’Plot’, u’id’: u’9bf6a048-035f-4d05-8168-3dd9dfd5e889’}, u’attr’: u’above’}]}
Traceback (most recent call last):
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/protocol/server_handler.py”, line 38, in handle
work = yield handler(message, connection)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/gen.py”, line 1008, in run
value = future.result()
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/concurrent.py”, line 232, in result
raise_exc_info(self._exc_info)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/gen.py”, line 1017, in run
yielded = self.gen.send(value)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/session.py”, line 42, in _needs_document_lock_wrapper
result = yield yield_for_all_futures(func(self, *args, **kwargs))
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/session.py”, line 212, in _handle_patch
message.apply_to_document(self.document)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/protocol/messages/patch_doc.py”, line 80, in apply_to_document
doc.apply_json_patch(self.content)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/document.py”, line 837, in apply_json_patch
raise RuntimeError(“Cannot apply patch to %s which is not in the document” % (str(patched_id)))
RuntimeError: Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document

``

And console errors in the client:

Bokeh: Unhandled ERROR reply to C1299A1D93DD4D7483B1893454CF55AF: RuntimeError(‘Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document’,)

``

Does this seem like a bug, or am I misusing functionality?

I suspect that I could alleviate this issue to some degree if I re-wrote my update function to do the graph calculation another thread, or to make use of co-routines, but it’s clear to me how best to do this. Any ideas?

Thanks,

Peter

You received this message because you are subscribed to the Google Groups “Bokeh Discussion - Public” group.

To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].

To post to this group, send email to [email protected].

To view this discussion on the web visit https://groups.google.com/a/continuum.io/d/msgid/bokeh/60327a7d-ace1-4363-842e-cc8a23ab606f%40continuum.io.

For more options, visit https://groups.google.com/a/continuum.io/d/optout.

Havoc Pennington

Senior Software Architect

Hi Havoc,

I think you are correct about this being a race condition. I don’t have very solid evidence to back this up, but intuition tells me these are the steps that are occurring:

  1. application enters a blocking call to perform some analysis

  2. application finishes blocking call and sends update 1

  3. application immediately starts work on another blocking call

  4. client receives update 1

  5. client makes some change in response to update 1 (changing the bounds of the plot or something) and sends update 2

  6. application doesn’t act on update 2 since it’s still in a blocking call

  7. application finishes blocking call and sends update 3, which contains the new plot line

  8. application processes update 2, which doesn’t contain the new line

  9. since the application believes there is a new line, it triggers update 4, which does not contain the new line

  10. client receives update 3, which contains the new plot line

  11. since the client believes there is no new line, it triggers update 5 which includes the new line

This is just speculation since I don’t have a strong grasp of the server protocol, or how to debug the interactions in a more nuanced level that just running tcpdump.

Whatever the cause, I was able to come up with a workaround. I do the query and analysis in a separate thread, and use Document.add_timeout_callback to periodically check for a result. Once the result is available, I update the document. This way, the on_click/on_change callbacks can return immediately rather than blocking on the analysis step, and there’s lower likelihood that the main event loop in the application will send out an update while there is already an update from the client waiting to be processed.

-Peter

···

On Thursday, January 21, 2016 at 4:41:50 PM UTC-5, Havoc Pennington wrote:

Hi,

This is a bug I think (similar to / the same as https://github.com/bokeh/bokeh/issues/3679 ?)

It’s hard to speculate on how to work around it without debugging why it happens. It looks complicated enough that someone will really have to schedule time to dig in on it, though. Step 1 is a test case so thanks for that!

It may be that we need to be quieter about events on no-longer-in-the-document models, because if a model is removed there could always be more events queued up for it, and the right thing to do would be to ignore them. But, I wouldn’t just turn off this error without first understanding the sequence of events here and be sure that’s what’s happening, and ideally we’d do something like specifically track removed model ids for a period of time so we could silence the error only for removed ids, not for out-of-the-blue ids that we really shouldn’t have been sent an event for at all.

The only way this would be an application bug is if two pieces of code are modifying the same model’s property at the same time; right now bokeh server really can’t deal with that in a sensible way (and it isn’t clear what a sensible way would be). However here I think only your client script is modifying the children of the plotWrapper, so that should be fine. Maybe it’s as simple as a race where the browser client is still sending changes for a plot that’s been removed elsewhere, and so when those changes arrive they should be ignored.

Havoc

On Thu, Jan 21, 2016 at 3:45 PM, [email protected] wrote:

Hi all,

I’m attempting to use the bokeh server to present a bokeh application with the following workflow:

  1. display a graph,
  1. display controls,
  1. when the user updates a control, trigger an event in the application:

a) run a query and do some analytics to build a new graph,

b) push the new graph to the client through the bokeh server.

The application works great for the most part, but if I change multiple controls too quickly (i.e. if I change a control in the client while the application is still building the graph for an earlier control change) then the application and client get into an infinite loop where they send PATCH-DOC messages back and forth, writing and overwriting the contents of the graph each time. I’ve written a small test case:

import time
import numpy
import datetime
import bokeh.client
import bokeh.plotting
import bokeh.models.widgets

n = 3
m = 5

numpy.random.seed(1)

data = numpy.cumsum(numpy.random.randn(n, m), axis = 1)

plot = bokeh.plotting.Figure()
plotWrapper = bokeh.models.widgets.HBox(plot)
buttons = bokeh.models.widgets.CheckboxButtonGroup(labels = map(str, range(n)), active = [0])

class Updater(object):

def __init__(self):
    self.__count = 0
       
def __call__(self, *args, **kwargs):
    print datetime.datetime.now(), "Starting update", self.__count, args, kwargs
    plot = bokeh.plotting.Figure()
    for index in buttons.active:
        print datetime.datetime.now(), "Adding", index
        plot.line(range(m), data[index])
    print datetime.datetime.now(), "Sleeping"
    time.sleep(5)
    plotWrapper.children = [plot]
    print datetime.datetime.now(), "Done with update", self.__count
    self.__count += 1

update = Updater()
update()
buttons.on_click(update)

controls = bokeh.models.widgets.VBoxForm(buttons)
app = bokeh.models.widgets.VBox(controls, plotWrapper)
doc = bokeh.plotting.curdoc()
doc.add_root(app)

session = bokeh.client.push_session(doc)
print session.id
session.loop_until_closed()

``

If you start the bokeh server and run that script on the same machine, and then open a tab to http://localhost:5006/?bokeh-session-id= with the session ID printed by the application, you will see three checkbox buttons (with one active), and a graph with one line. If you quickly click the other two buttons, five seconds later a second line will show up, and then five seconds after that the client will start experiencing problems: it may temporarily show all three lines, and then flash back and forth between the two lines and the three lines, or it may just hang, taking up a full core.

The application will write output similar to the following, and will continue to use significant cpu:

2016-01-21 20:24:07.586236 Starting update 0 () {}
2016-01-21 20:24:07.590865 Adding 0
2016-01-21 20:24:07.593379 Sleeping
2016-01-21 20:24:12.598626 Done with update 0

2016-01-21 20:24:45.647457 Starting update 1 ([0, 1],) {}
2016-01-21 20:24:45.651199 Adding 0
2016-01-21 20:24:45.652940 Adding 1
2016-01-21 20:24:45.654289 Sleeping
2016-01-21 20:24:50.663665 Done with update 1
2016-01-21 20:24:50.664638 Starting update 2 ([0, 1, 2],) {}
2016-01-21 20:24:50.668122 Adding 0
2016-01-21 20:24:50.669778 Adding 1
2016-01-21 20:24:50.671121 Adding 2
2016-01-21 20:24:50.672656 Sleeping
2016-01-21 20:24:55.682917 Done with update 2

``

The bokeh server will also use significant CPU.

If I do a tcpdump, I see the PATCH-DOC json messages going back and forth between both the client and the server, and between the application and the server. On some runs, I will see runtime errors in the server log:

ERROR:bokeh.server.protocol.server_handler:error handling message Message ‘PATCH-DOC’ (revision 1): RuntimeError(‘Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document’,)
DEBUG:bokeh.server.protocol.server_handler: message header {u’msgid’: u’C1299A1D93DD4D7483B1893454CF55AF’, u’msgtype’: u’PATCH-DOC’} content {u’references’: [{u’attributes’: {u’name’: u’title_panel’, u’tags’: }, u’type’: u’LayoutBox’, u’id’: u’LayoutBox-7F186730CFC743078DB472E0DD22F4AB’}], u’events’: [{u’new’: [{u’type’: u’LayoutBox’, u’id’: u’LayoutBox-7F186730CFC743078DB472E0DD22F4AB’}], u’kind’: u’ModelChanged’, u’model’: {u’subtype’: u’Figure’, u’type’: u’Plot’, u’id’: u’9bf6a048-035f-4d05-8168-3dd9dfd5e889’}, u’attr’: u’above’}]}
Traceback (most recent call last):
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/protocol/server_handler.py”, line 38, in handle
work = yield handler(message, connection)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/gen.py”, line 1008, in run
value = future.result()
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/concurrent.py”, line 232, in result
raise_exc_info(self._exc_info)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/tornado-4.3-py2.7-linux-x86_64.egg/tornado/gen.py”, line 1017, in run
yielded = self.gen.send(value)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/session.py”, line 42, in _needs_document_lock_wrapper
result = yield yield_for_all_futures(func(self, *args, **kwargs))
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/session.py”, line 212, in _handle_patch
message.apply_to_document(self.document)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/server/protocol/messages/patch_doc.py”, line 80, in apply_to_document
doc.apply_json_patch(self.content)
File “/data/sfw/Python-2.7.6/deploy/lib/python2.7/site-packages/bokeh/document.py”, line 837, in apply_json_patch
raise RuntimeError(“Cannot apply patch to %s which is not in the document” % (str(patched_id)))
RuntimeError: Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document

``

And console errors in the client:

Bokeh: Unhandled ERROR reply to C1299A1D93DD4D7483B1893454CF55AF: RuntimeError(‘Cannot apply patch to 9bf6a048-035f-4d05-8168-3dd9dfd5e889 which is not in the document’,)

``

Does this seem like a bug, or am I misusing functionality?

I suspect that I could alleviate this issue to some degree if I re-wrote my update function to do the graph calculation another thread, or to make use of co-routines, but it’s clear to me how best to do this. Any ideas?

Thanks,

Peter

You received this message because you are subscribed to the Google Groups “Bokeh Discussion - Public” group.

To unsubscribe from this group and stop receiving emails from it, send an email to