Skip to content

Add server_url to plotly.io.orca to allow for external orca server #1850

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

Merged
merged 6 commits into from
Nov 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 134 additions & 72 deletions packages/python/plotly/plotly/io/_orca.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,48 @@ def save(self):
)
)

@property
def server_url(self):
"""
The server URL to use for an external orca server, or None if orca
should be managed locally

Overrides executable, port, timeout, mathjax, topojson,
and mapbox_access_token

Returns
-------
str or None
"""
return self._props.get("server_url", None)

@server_url.setter
def server_url(self, val):

if val is None:
self._props.pop("server_url", None)
return
if not isinstance(val, str):
raise ValueError(
"""
The server_url property must be a string, but received value of type {typ}.
Received value: {val}""".format(
typ=type(val), val=val
)
)

if not val.startswith("http://") and not val.startswith("https://"):
val = "http://" + val

shutdown_server()
self.executable = None
self.port = None
self.timeout = None
self.mathjax = None
self.topojson = None
self.mapbox_access_token = None
self._props["server_url"] = val

@property
def port(self):
"""
Expand Down Expand Up @@ -777,6 +819,7 @@ def __repr__(self):
return """\
orca configuration
------------------
server_url: {server_url}
executable: {executable}
port: {port}
timeout: {timeout}
Expand All @@ -795,6 +838,7 @@ def __repr__(self):
config_file: {config_file}

""".format(
server_url=self.server_url,
port=self.port,
executable=self.executable,
timeout=self.timeout,
Expand Down Expand Up @@ -1343,62 +1387,65 @@ def ensure_server():
if status.state == "unvalidated":
validate_executable()

# Acquire lock to make sure that we keep the properties of orca_state
# consistent across threads
with orca_lock:
# Cancel the current shutdown timer, if any
if orca_state["shutdown_timer"] is not None:
orca_state["shutdown_timer"].cancel()
if not config.server_url:
# Acquire lock to make sure that we keep the properties of orca_state
# consistent across threads
with orca_lock:
# Cancel the current shutdown timer, if any
if orca_state["shutdown_timer"] is not None:
orca_state["shutdown_timer"].cancel()

# Start a new server process if none is active
if orca_state["proc"] is None:

# Determine server port
if config.port is None:
orca_state["port"] = find_open_port()
else:
orca_state["port"] = config.port

# Build orca command list
cmd_list = status._props["executable_list"] + [
"serve",
"-p",
str(orca_state["port"]),
"--plotly",
config.plotlyjs,
"--graph-only",
]

# Start a new server process if none is active
if orca_state["proc"] is None:
if config.topojson:
cmd_list.extend(["--topojson", config.topojson])

# Determine server port
if config.port is None:
orca_state["port"] = find_open_port()
else:
orca_state["port"] = config.port

# Build orca command list
cmd_list = status._props["executable_list"] + [
"serve",
"-p",
str(orca_state["port"]),
"--plotly",
config.plotlyjs,
"--graph-only",
]

if config.topojson:
cmd_list.extend(["--topojson", config.topojson])

if config.mathjax:
cmd_list.extend(["--mathjax", config.mathjax])

if config.mapbox_access_token:
cmd_list.extend(["--mapbox-access-token", config.mapbox_access_token])

# Create subprocess that launches the orca server on the
# specified port.
DEVNULL = open(os.devnull, "wb")
with orca_env():
orca_state["proc"] = subprocess.Popen(cmd_list, stdout=DEVNULL)

# Update orca.status so the user has an accurate view
# of the state of the orca server
status._props["state"] = "running"
status._props["pid"] = orca_state["proc"].pid
status._props["port"] = orca_state["port"]
status._props["command"] = cmd_list

# Create new shutdown timer if a timeout was specified
if config.timeout is not None:
t = threading.Timer(config.timeout, shutdown_server)
# Make it a daemon thread so that exit won't wait for timer to
# complete
t.daemon = True
t.start()
orca_state["shutdown_timer"] = t
if config.mathjax:
cmd_list.extend(["--mathjax", config.mathjax])

if config.mapbox_access_token:
cmd_list.extend(
["--mapbox-access-token", config.mapbox_access_token]
)

# Create subprocess that launches the orca server on the
# specified port.
DEVNULL = open(os.devnull, "wb")
with orca_env():
orca_state["proc"] = subprocess.Popen(cmd_list, stdout=DEVNULL)

# Update orca.status so the user has an accurate view
# of the state of the orca server
status._props["state"] = "running"
status._props["pid"] = orca_state["proc"].pid
status._props["port"] = orca_state["port"]
status._props["command"] = cmd_list

# Create new shutdown timer if a timeout was specified
if config.timeout is not None:
t = threading.Timer(config.timeout, shutdown_server)
# Make it a daemon thread so that exit won't wait for timer to
# complete
t.daemon = True
t.start()
orca_state["shutdown_timer"] = t


@retrying.retry(wait_random_min=5, wait_random_max=10, stop_max_delay=60000)
Expand All @@ -1409,9 +1456,12 @@ def request_image_with_retrying(**kwargs):
"""
from requests import post

server_url = "http://{hostname}:{port}".format(
hostname="localhost", port=orca_state["port"]
)
if config.server_url:
server_url = config.server_url
else:
server_url = "http://{hostname}:{port}".format(
hostname="localhost", port=orca_state["port"]
)

request_params = {k: v for k, v, in kwargs.items() if v is not None}
json_str = json.dumps(request_params, cls=_plotly_utils.utils.PlotlyJSONEncoder)
Expand Down Expand Up @@ -1512,29 +1562,41 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru
# Get current status string
status_str = repr(status)

# Check if the orca server process exists
pid_exists = psutil.pid_exists(status.pid)

# Raise error message based on whether the server process existed
if pid_exists:
if config.server_url:
raise ValueError(
"""
Plotly.py was unable to communicate with the orca server at {server_url}

Please check that the server is running and accessible.
""".format(
server_url=config.server_url
)
)

else:
# Check if the orca server process exists
pid_exists = psutil.pid_exists(status.pid)

# Raise error message based on whether the server process existed
if pid_exists:
raise ValueError(
"""
For some reason plotly.py was unable to communicate with the
local orca server process, even though the server process seems to be running.

Please review the process and connection information below:

{info}
""".format(
info=status_str
info=status_str
)
)
)
else:
# Reset the status so that if the user tries again, we'll try to
# start the server again
reset_status()
raise ValueError(
"""
else:
# Reset the status so that if the user tries again, we'll try to
# start the server again
reset_status()
raise ValueError(
"""
For some reason the orca server process is no longer running.

Please review the process and connection information below:
Expand All @@ -1543,9 +1605,9 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru
plotly.py will attempt to start the local server process again the next time
an image export operation is performed.
""".format(
info=status_str
info=status_str
)
)
)

# Check response
# --------------
Expand Down
46 changes: 46 additions & 0 deletions packages/python/plotly/plotly/tests/test_orca/test_orca_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
import time
import psutil
import pytest
import plotly.graph_objects as go


# Fixtures
# --------
from plotly.io._orca import find_open_port, which, orca_env


@pytest.fixture()
def setup():
# Set problematic environment variables
Expand Down Expand Up @@ -150,3 +154,45 @@ def test_server_timeout_shutdown():

# Check that ping is no longer answered
assert not ping_pongs(server_url)


def test_external_server_url():
# Build server url
port = find_open_port()
server_url = "http://{hostname}:{port}".format(hostname="localhost", port=port)

# Build external orca command
orca_path = which("orca")
cmd_list = [orca_path] + [
"serve",
"-p",
str(port),
"--plotly",
pio.orca.config.plotlyjs,
"--graph-only",
]

# Run orca as subprocess to simulate external orca server
DEVNULL = open(os.devnull, "wb")
with orca_env():
proc = subprocess.Popen(cmd_list, stdout=DEVNULL)

# Start plotly managed orca server so we can ensure it gets shut down properly
pio.orca.config.port = port
pio.orca.ensure_server()
assert pio.orca.status.state == "running"

# Configure orca to use external server
pio.orca.config.server_url = server_url

# Make sure that the locally managed orca server has been shutdown and the local
# config options have been cleared
assert pio.orca.status.state == "unvalidated"
assert pio.orca.config.port is None

fig = go.Figure()
img_bytes = pio.to_image(fig, format="svg")
assert img_bytes.startswith(b"<svg class")

# Kill server orca process
proc.terminate()