Skip to content
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

v0.7.0 #226

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0f760f6
bump version
JoshKarpel May 27, 2020
86f16be
make failed map submission safer
JoshKarpel May 27, 2020
2c7b361
loosen dependency version requirements
JoshKarpel Jul 8, 2020
c2d1eca
Merge branch 'master' into v0.7.0
JoshKarpel Aug 10, 2020
ec970cb
Tests pass, html stable produced
elin1231 Aug 14, 2020
b5615b4
Uodated maps for html representation divided into header and grid
elin1231 Aug 17, 2020
4ea13c4
Merge branch 'master' into v0.7.0
JoshKarpel Aug 17, 2020
72d8c23
Merge branch 'v0.7.0' into jupyter_lab_widget
JoshKarpel Aug 17, 2020
af663d3
changes made based on PR
elin1231 Aug 20, 2020
212d95a
Pair programming Josh PR fixes
elin1231 Aug 20, 2020
f10682b
Merge pull request #227 from elin1231/jupyter_lab_widget
JoshKarpel Aug 20, 2020
b993c02
install nodejs and npm in the dev container and build the ipywidgets …
JoshKarpel Aug 20, 2020
8e00f4c
Jupyter lab widget wrapped HTML code in ipywidget (#228)
elin1231 Aug 27, 2020
5fa3096
Live update working
elin1231 Aug 27, 2020
1ba660f
Updated to remove duplicate code. Working live updating for status an…
elin1231 Aug 28, 2020
5095a54
implement background thread for updating widgets; add widget to Map.w…
JoshKarpel Aug 31, 2020
8901122
Merge pull request #230 from elin1231/jupyter_lab_widget_live_update_…
JoshKarpel Sep 1, 2020
0e2d0c9
Merge branch 'master' into v0.7.0
matyasselmeci Jun 24, 2021
38ce138
Merge branch 'master' into v0.7.0
matyasselmeci Jun 24, 2021
3072125
Drop --use-feature=2020-resolver flag because it's now the default
matyasselmeci Jun 25, 2021
d1af00e
Don't install the jupyterlab-manager labextension -- it requires node…
matyasselmeci Jun 25, 2021
8fc7871
Merge pull request #239 from matyasselmeci/pr/v0.7.0-fixups
matyasselmeci Jun 25, 2021
3bf422c
Merge branch 'master' into v0.7.0
matyasselmeci Jun 29, 2021
21717c2
Merge branch 'master' into v0.7.0
matyasselmeci Nov 4, 2022
502f24e
Add jupyterlab-manager back -- Debian bullseye has Node 12
matyasselmeci Nov 4, 2022
17a027a
Add a build arg to disable NodeJS
matyasselmeci Nov 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 9 additions & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ FROM python:${PYTHON_VERSION}
# build config
ARG HTCONDOR_VERSION=9.0

ARG DISABLE_NODEJS=

# switch to root to do root-level config
USER root

Expand All @@ -39,9 +41,9 @@ ENV USER=mapper \
PATH="/home/mapper/.local/bin:${PATH}" \
PYTHONPATH="/home/mapper/htmap:${PYTHONPATH}"
RUN : \
&& groupadd ${USER} \
&& useradd -m -g ${USER} ${USER} \
&& :
&& groupadd ${USER} \
&& useradd -m -g ${USER} ${USER} \
&& :

# switch to the user, don't need root anymore
USER ${USER}
Expand All @@ -66,6 +68,9 @@ RUN : \
requirement="htcondor~=${htcondor_version_major}.0.0"; \
# ^^ gets translated into e.g. >=9.0.0,<9.1 \
fi \
&& python -m pip install --user --no-cache-dir --disable-pip-version-check "/home/${USER}/htmap[tests,docs]" "$requirement"
&& python -m pip install --user --no-cache-dir --disable-pip-version-check "/home/${USER}/htmap[tests,docs,widgets]" "$requirement" \
&& jupyter nbextension enable --py widgetsnbextension \
&& [ "X${DISABLE_NODEJS}" != X ] || jupyter labextension install --minimize=False @jupyter-widgets/jupyterlab-manager \
&& :

WORKDIR /home/${USER}/htmap
3 changes: 3 additions & 0 deletions docker/install-htcondor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export DEBIAN_FRONTEND=noninteractive

apt-get update
apt-get -y install --no-install-recommends vim less git gnupg wget ca-certificates locales graphviz pandoc strace
if [[ ! $DISABLE_NODEJS ]]; then
apt-get -y install --no-install-recommends nodejs npm
fi
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
locale-gen
wget -qO - "https://research.cs.wisc.edu/htcondor/repo/keys/HTCondor-${HTCONDOR_VERSION}-Key" | apt-key add -
Expand Down
8 changes: 7 additions & 1 deletion dr
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ set -e

docker build -t ${CONTAINER_TAG} --file docker/Dockerfile .

docker run -it --rm --mount type=bind,src="$PWD",dst=/home/mapper/htmap -p 8000:8000 ${CONTAINER_TAG} $@
docker run \
-it --rm \
--mount type=bind,src="$PWD",dst=/home/mapper/htmap \
-p 8000:8000 \
-p 8888:8888 \
${CONTAINER_TAG} \
$@
2 changes: 1 addition & 1 deletion htmap/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def create_map(

tags.tag_file_path(tag).write_text(str(uid))

m = maps.Map(tag=tag, map_dir=map_dir,)
m = maps.Map(tag=tag, map_dir=map_dir)

if transient:
m._make_transient()
Expand Down
200 changes: 179 additions & 21 deletions htmap/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import inspect
import logging
import shutil
import threading
import time
import weakref
from copy import copy
Expand Down Expand Up @@ -70,6 +71,32 @@ def maps_by_tag() -> Dict[str, "Map"]:
return {m.tag: m for m in MAPS}


def update_widgets():
while True:
for map in MAPS.copy():
try:
_, update = map._widget()

if update is not None:
update()
except:
logger.exception("Widget update thread encountered error!")

time.sleep(settings["WAIT_TIME"])


WIDGET_UPDATE_THREAD = threading.Thread(target=update_widgets, daemon=True)


def start_widget_update_thread():
try:
if not WIDGET_UPDATE_THREAD.is_alive():
WIDGET_UPDATE_THREAD.start()
except RuntimeError:
# Someone else started the thread before we did, no worries
pass


@_protect_map_after_remove
class Map(collections.abc.Sequence):
"""
Expand Down Expand Up @@ -105,6 +132,8 @@ def __init__(
self._stderr: MapStdErr = MapStdErr(self)
self._output_files: MapOutputFiles = MapOutputFiles(self)

self._cached_widget = (None, None)

MAPS.add(self)

@property
Expand Down Expand Up @@ -140,10 +169,121 @@ def load(cls, tag: str) -> "Map":

logger.debug(f"Loaded map {tag} from {map_dir}")

return cls(tag=tag, map_dir=map_dir,)
return cls(tag=tag, map_dir=map_dir)

def __repr__(self):
return f"{self.__class__.__name__}(tag = {self.tag})"
return f"{self.__class__.__name__}(tag={self.tag})"

def status(self):
"""Display a string containing the number of jobs in each status."""
counts = collections.Counter(self.component_statuses)
stat = " | ".join(
f"{str(js)} = {counts[js]}" for js in state.ComponentStatus.display_statuses()
)
plain = f"{self.__class__.__name__} {self.tag} ({len(self)} components): {stat}"

if not utils.is_jupyter():
print(plain)
return

from IPython.display import display

widget, _ = self._widget()

if widget is not None:
display(widget)
return

data = {"text/plain": plain, "text/html": self._repr_html_()}

display(data, raw=True)

def _ipython_display_(self, **kwargs):
self.status()

def _widget(self):
try:
from ipywidgets import Layout, VBox, widgets
except ImportError:
return self._cached_widget

if self._cached_widget != (None, None):
return self._cached_widget

table = widgets.HTML(value=self._repr_html_(), layout=Layout(min_width="150px"))

pbar = widgets.IntProgress(
value=0, min=0, max=len(self), orientation="horizontal", layout=Layout(width="90%"),
)
widget = VBox([table, pbar])

def update():
table.value = self._repr_html_()
pbar.value = len(self.components_by_status().get(state.ComponentStatus.COMPLETED, []))

update()

self._cached_widget = widget, update

start_widget_update_thread()

return self._cached_widget

def _repr_html_(self):
return self._html_table()

def _html_table(self):
table = [
# Hacked together by looking at the classes of the parent div in
# the version formatted by Jupyter... probably not very stable.
'<div class="lm-Widget p-Widget jp-RenderedHTMLCommon jp-RenderedHTML jp-mod-trusted jp-OutputArea-output">',
'<table cellpadding="5" border = "1">',
" <thead>",
f" <tr>{self._html_table_header()}</tr>",
" </thead>",
" <tbody>",
f" <tr>{self._html_table_body()}</tr>",
" </tbody>",
"</table>",
"</div>",
]

return "\n".join(table)

@staticmethod
def _html_table_header():
return "<th> TAG </th>" + "".join(
f"<td> {h} </td>"
for h in [
*state.ComponentStatus.display_statuses(),
"Local Data",
"Max Memory",
"Max Runtime",
"Total Runtime",
]
)

def _html_table_body(self):
sc = collections.Counter(self.component_statuses)

local_data = utils.num_bytes_to_str(self.local_data)
max_memory = utils.num_bytes_to_str(max(self.memory_usage) * 1024 * 1024)
max_runtime = str(max(self.runtime))
total_runtime = str(sum(self.runtime, datetime.timedelta()))

return f'<th align = "center"> {self.tag} </th>' + "".join(
f'<td align = "center"> {h} </td>'
for h in [
*[
sc[component_state]
for component_state in state.ComponentStatus.display_statuses()
],
local_data,
max_memory,
max_runtime,
total_runtime,
]
)

def __gt__(self, other):
return self.tag > other.tag
Expand Down Expand Up @@ -222,7 +362,7 @@ def is_active(self) -> bool:
def wait(
self,
timeout: utils.Timeout = None,
show_progress_bar: bool = False,
show_progress_bar: Optional[bool] = None,
holds_ok: bool = False,
errors_ok: bool = False,
) -> None:
Expand All @@ -240,6 +380,8 @@ def wait(
If ``None``, wait forever.
show_progress_bar
If ``True``, a progress bar will be displayed.
If ``None`` (the default), a progress bar will be displayed if you
are running Python interactively (e.g., in a REPL or Jupyter session).
holds_ok
If ``True``, will not raise exceptions if components are held.
errors_ok
Expand All @@ -248,11 +390,22 @@ def wait(
start_time = time.time()
timeout = utils.timeout_to_seconds(timeout)

if show_progress_bar is None and utils.is_interactive_session():
show_progress_bar = True

try:
pbar = None
if show_progress_bar:
pbar = tqdm(desc=self.tag, total=len(self), unit="component", ascii=True,)
# TODO: what if no widget
widget, update = self._widget()
if utils.is_jupyter() and widget is not None:
from IPython.display import display

display(widget)
else:
pbar = tqdm(desc=self.tag, total=len(self), unit="component", ascii=True,)

previous_pbar_len = 0
previous_pbar_len = 0

ok_statuses = {state.ComponentStatus.COMPLETED}
if holds_ok:
Expand All @@ -262,10 +415,15 @@ def wait(

while True:
num_incomplete = sum(cs not in ok_statuses for cs in self.component_statuses)

if show_progress_bar:
pbar_len = self._num_components - num_incomplete
pbar.update(pbar_len - previous_pbar_len)
previous_pbar_len = pbar_len
if pbar:
pbar_len = self._num_components - num_incomplete
pbar.update(pbar_len - previous_pbar_len)
previous_pbar_len = pbar_len
else:
update()

if num_incomplete == 0:
break

Expand All @@ -284,7 +442,7 @@ def wait(

time.sleep(settings["WAIT_TIME"])
finally:
if show_progress_bar:
if show_progress_bar and pbar:
pbar.close()

def _wait_for_component(self, component: int, timeout: utils.Timeout = None) -> None:
Expand Down Expand Up @@ -603,16 +761,6 @@ def job(x):
status: tuple(sorted(components)) for status, components in status_to_components.items()
}

def status(self) -> str:
"""Return a string containing the number of jobs in each status."""
counts = collections.Counter(self.component_statuses)
stat = " | ".join(
f"{str(js)} = {counts[js]}" for js in state.ComponentStatus.display_statuses()
)
msg = f"{self.__class__.__name__} {self.tag} ({len(self)} components): {stat}"

return utils.rstr(msg)

@property
def holds(self) -> Dict[int, holds.ComponentHold]:
"""
Expand Down Expand Up @@ -922,8 +1070,18 @@ def _submit(self, components: Optional[Iterable[int]] = None) -> None:
# if we fail to write the cluster id for any reason, abort the submit
try:
htio.append_cluster_id(self._map_dir, new_cluster_id)
except BaseException as e:
condor.get_schedd().act(htcondor.JobAction.Remove, f"ClusterId=={new_cluster_id}")
except BaseException as write_exception:
logger.exception(
f"Failed to write new cluster id {new_cluster_id} for map {self.tag}, aborting submission"
)
try:
condor.get_schedd().act(htcondor.JobAction.Remove, f"ClusterId=={new_cluster_id}")
except BaseException as remove_exception:
logger.exception(
f"Was not able to abort submission of cluster id {new_cluster_id} for map {self.tag}"
)
raise remove_exception
raise write_exception

logger.debug(
f"Submitted {len(sliced_itemdata)} components (out of {self._num_components}) from map {self.tag}"
Expand Down
3 changes: 2 additions & 1 deletion htmap/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ def _event_log_path(self):
def _read_events(self):
with self._event_reader_lock: # no thread can be in here at the same time as another
if self._event_reader is None:
logger.debug(f"Created event log reader for map {self.map.tag}")
self._event_log_path.touch(exist_ok=True)
self._event_reader = htcondor.JobEventLog(self._event_log_path.as_posix())
logger.debug(f"Created event log reader for map {self.map.tag}")

with utils.Timer() as timer:
handled_events = self._handle_events()
Expand Down
16 changes: 16 additions & 0 deletions htmap/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,22 @@ def is_interactive_session() -> bool:
)


def is_jupyter() -> bool:
# https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook/24937408
# This seems quite fragile, but it also seems hard to determine otherwise...
# I would not be shocked if this breaks in the future.
try:
shell = get_ipython().__class__.__name__
if shell == "ZMQInteractiveShell":
return True # Jupyter notebook or qtconsole
elif shell == "TerminalInteractiveShell":
return False # Terminal running IPython
else:
return False # Something else...
except NameError:
return False # Probably standard Python interpreter


def enable_debug_logging():
logger = logging.getLogger("htmap")
logger.setLevel(logging.DEBUG)
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ tests =
pytest-timeout
pytest-watch
pytest-xdist
widgets =
ipywidgets
jupyterlab>=2

[options.package_data]
* =
Expand Down