Skip to content

Commit

Permalink
[card-refresh] implement card reload policy
Browse files Browse the repository at this point in the history
  • Loading branch information
tuulos authored and valayDave committed Sep 26, 2023
1 parent 36ccce5 commit 2566019
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 16 deletions.
33 changes: 29 additions & 4 deletions metaflow/plugins/cards/card_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,13 +378,38 @@ def wrapper(*args, **kwargs):


def update_card(mf_card, mode, task, data, timeout_value=None):
def _reload_token():
if data["render_seq"] == "final":
# final data update should always trigger a card reload to show
# the final card, hence a different token for the final update
return "final"
elif mf_card.RELOAD_POLICY == mf_card.RELOAD_POLICY_ALWAYS:
return "render-seq-%s" % data["render_seq"]
elif mf_card.RELOAD_POLICY == mf_card.RELOAD_POLICY_NEVER:
return "never"
elif mf_card.RELOAD_POLICY == mf_card.RELOAD_POLICY_ONCHANGE:
return mf_card.reload_content_token(task, data)

def _add_token_html(html):
if html is not None:
return html.replace(mf_card.RELOAD_POLICY_TOKEN, _reload_token())

def _add_token_json(json_msg):
if json_msg is not None:
return {"reload_token": _reload_token(), "data": json_msg}

def _call():
# compatibility with old render()-method that doesn't accept the data arg
new_render = "data" in inspect.getfullargspec(mf_card.render).args
if mode == "render" and not new_render:
return mf_card.render(task)
else:
return getattr(mf_card, mode)(task, data=data)
if mode == "render":
if new_render:
return _add_token_html(mf_card.render(task, data))
else:
return _add_token_html(mf_card.render(task))
elif mode == "render_runtime":
return _add_token_html(mf_card.render_runtime(task, data))
elif mode == "refresh":
return _add_token_json(mf_card.refresh(task, data))

if timeout_value is None or timeout_value < 0:
return _call()
Expand Down
16 changes: 9 additions & 7 deletions metaflow/plugins/cards/card_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def task_pre_step(
):
card_type = self.attributes["type"]
card_class = get_card_class(card_type)

self._is_runtime_card = False
if card_class is not None: # Card type was not found
if card_class.ALLOW_USER_COMPONENTS:
self._is_editable = True
Expand Down Expand Up @@ -189,11 +191,11 @@ def task_pre_step(
def task_finished(
self, step_name, flow, graph, is_task_ok, retry_count, max_user_code_retries
):
if not is_task_ok:
return
return self._card_proc("render")
if is_task_ok:
self._card_proc("render")
self._card_proc("refresh", final=True)

def _card_proc(self, mode):
def _card_proc(self, mode, final=False):
if mode != "render" and not self._is_runtime_card:
# silently ignore runtime updates for cards that don't support them
return
Expand All @@ -204,7 +206,7 @@ def _card_proc(self, mode):
else:
component_strings = current.card._serialize_components(self._card_uuid)

data = current.card._get_latest_data(self._card_uuid)
data = current.card._get_latest_data(self._card_uuid, final=final)
runspec = "/".join([current.run_id, current.step_name, current.task_id])
self._run_cards_subprocess(mode, runspec, component_strings, data)

Expand Down Expand Up @@ -245,7 +247,7 @@ def _run_cards_subprocess(self, mode, runspec, component_strings, data=None):
"w", suffix=".json", delete=False
)
json.dump(component_strings, components_file)
compotents_file.seek(0)
components_file.seek(0)
if data is not None:
data_file = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
json.dump(data, data_file)
Expand Down Expand Up @@ -329,7 +331,7 @@ def _run_command(self, cmd, env, wait=True, timeout=None):
# and timeout hasn't been reached
return "", False
else:
#print("CARD CMD", " ".join(cmd))
# print("CARD CMD", " ".join(cmd))
self._async_proc = subprocess.Popen(
cmd, env=env, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL
)
Expand Down
28 changes: 28 additions & 0 deletions metaflow/plugins/cards/card_modules/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,34 @@ class MetaflowCard(object):
JSON-encodable dictionary containing user-definable options for the class.
"""

# RELOAD_POLICY determines whether UIs should
# reload intermediate cards produced by render_runtime
# or whether they can just rely on data updates

# the UI may keep using the same card
# until the final card is produced
RELOAD_POLICY_NEVER = "never"

# the UI should reload card every time
# render_runtime() has produced a new card
RELOAD_POLICY_ALWAYS = "always"

# derive reload token from data and component
# content - force reload only when the content
# changes. The actual policy is card-specific,
# defined by the method reload_content_token()
RELOAD_POLICY_ONCHANGE = "onchange"

# this token will get replaced in the html with a unique
# string that is used to ensure that data updates and the
# card content matches
RELOAD_POLICY_TOKEN = "[METAFLOW_RELOAD_TOKEN]"

type = None

ALLOW_USER_COMPONENTS = False
IS_RUNTIME_CARD = False
RELOAD_POLICY = RELOAD_POLICY_NEVER

scope = "task" # can be task | run

Expand Down Expand Up @@ -78,6 +102,10 @@ def render_runtime(self, task, data):
def refresh(self, task, data):
return

# FIXME document
def reload_content_token(self, task, data):
return "content-token"


class MetaflowCardComponent(object):
def render(self):
Expand Down
13 changes: 8 additions & 5 deletions metaflow/plugins/cards/component_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self, card_proc, components=None):
self._latest_user_data = None
self._last_refresh = 0
self._last_render = 0
self._render_seq = 0

if components is None:
self._components = []
Expand All @@ -58,14 +59,16 @@ def refresh(self, data=None, force=False):
self._last_refresh = nu
# FIXME force render if components have changed
if force or nu - self._last_render > RUNTIME_CARD_RENDER_INTERVAL:
self._card_proc("render_runtime")
self._render_seq += 1
self._last_render = nu
self._card_proc("render_runtime")
else:
self._card_proc("refresh")

def _get_latest_data(self):
def _get_latest_data(self, final=False):
# FIXME add component data
return {"user": self._latest_user_data, "components": []}
seq = 'final' if final else self._render_seq
return {"user": self._latest_user_data, "components": [], "render_seq": seq}

def __iter__(self):
return iter(self._components)
Expand Down Expand Up @@ -418,11 +421,11 @@ def refresh(self, *args, **kwargs):
if self._default_editable_card is not None:
self._cards_components[self._default_editable_card].refresh(*args, **kwargs)

def _get_latest_data(self, card_uuid):
def _get_latest_data(self, card_uuid, final=False):
"""
Returns latest data so it can be used in the final render() call
"""
return self._cards_components[card_uuid]._get_latest_data()
return self._cards_components[card_uuid]._get_latest_data(final=final)

def _serialize_components(self, card_uuid):
"""
Expand Down

0 comments on commit 2566019

Please sign in to comment.