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

Implement and utilize countdown widget (for temporary deployments) #198

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
52 changes: 52 additions & 0 deletions home/widgets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""AiiDAlab basic widgets."""

import subprocess
from datetime import datetime
from threading import Timer

import ipywidgets as ipw
Expand Down Expand Up @@ -187,3 +189,53 @@ def _refresh_output(self, _=None):
self._output.value = (
self.template.format(text=self.value) if self.value else ""
)


class CountDownWidget(ipw.VBox):
"""A countdown w.r.t the AiiDAlab container creation time."""

TEMPLATE = "<h1 style='color: {color}'>Time remaining: <b>{countdown}</b></h1>"
FINAL_WARNING = "<h1 style='color: red'>AiiDAlab container shutdown imminent - please save your work!</h1>"

def __init__(self, lifetime: str, **kwargs):
self.reference_time = datetime.strptime(lifetime, "%H:%M:%S")

self.countdown = ipw.HTML()

super().__init__(
children=[
self.countdown,
],
**kwargs,
)

def start(self):
Timer(1, self._update_countdown).start()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Infinitely running background threads that update the UI state are unfortunately a really bad idea due to a possible memory leak described in https://github.com/aiidalab/issues/issues/13.

I am afraid timer like this needs to be in javascript. If I am not mistaken, as long as the timer is initialized properly, it shouldn't need any information from the backend, right?


def _update_countdown(self):
try:
remaining, time_running_out = self._remaining_time()
message = self.TEMPLATE.format(
color="red" if time_running_out else "black",
countdown=remaining,
)
except Exception:
remaining = None
message = "Failed to obtain remaining time."
self.countdown.value = message
if remaining != "00:00:00":
self.start()
else:
self.countdown.value = self.FINAL_WARNING

def _remaining_time(self) -> tuple[str, bool]:
process = subprocess.check_output(["ps", "-o", "etime=", "-p", "1"])
elapsed_str = process.decode().strip()
if len(elapsed_str) < 6:
elapsed_str = f"00:{elapsed_str}"
elapsed_time = datetime.strptime(elapsed_str, "%H:%M:%S")
Comment on lines +234 to +236
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also consider time longer than one day, (maybe even months).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What use case are you thinking of here? I'm not trying to make this general. This is specifically for demo server deployments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that our current demo server only supports 12 hours, but since this is in the AiiDAlab-home, it should be more general, because other people may use this feature, or, we may use this feature in another deployment longer than 12 hours.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can update this for "other people" down the road. For now, this is fine for our use case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just more caution for the aiidalab-home because every time we make a new release, the docker image must be upgraded.

But OK to me if you intend to leave this for the future.

remaining = self.reference_time - elapsed_time
if remaining.total_seconds() >= 0:
time_running_out = remaining.total_seconds() < 60 * 5
return (datetime.min + remaining).strftime("%H:%M:%S"), time_running_out
return "00:00:00", True
21 changes: 21 additions & 0 deletions start.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@
" parsed_url = urlparse.parse_qs(url.query)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from yaml import safe_load\n",
"\n",
"from home.widgets import CountDownWidget\n",
"\n",
"# For temporary deployments (e.g. demo server), duration is read from a local\n",
"# config file. This will initiate the countdown widget.\n",
"demo_server_config_file = Path.home() / \".aiidalab\" / \"demo-server-config.yml\"\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the QEapp, we don't want to hardcode the demo-server in the app, so I would suggest using aiidalab-home.yaml, or aiidalab.yaml directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is explicitly for demo servers (not our demo server, but for demo servers)

"if demo_server_config_file.exists():\n",
" demo_server_config: dict = safe_load(demo_server_config_file.read_text()) # type: ignore\n",
" if \"lifetime\" in demo_server_config:\n",
" countdown = CountDownWidget(lifetime=demo_server_config[\"lifetime\"])\n",
" countdown.start()\n",
" display(countdown)"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down
Loading