From 40bc31468898b55b9aefda12df88f9b4687917ba Mon Sep 17 00:00:00 2001 From: Andrei Neagu Date: Mon, 23 Oct 2023 15:20:00 +0200 Subject: [PATCH] service exposes inactivity --- .github/workflows/check-image.yml | 6 +-- .osparc/jupyter-math/runtime.yml | 5 ++ Dockerfile | 7 ++- Makefile | 4 +- docker/entrypoint.bash | 1 + docker/inactivity.py | 9 ++++ docker/kernel_cehcker.py | 90 +++++++++++++++++++++++++++++++ 7 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 docker/inactivity.py create mode 100644 docker/kernel_cehcker.py diff --git a/.github/workflows/check-image.yml b/.github/workflows/check-image.yml index d64dc01..b460c9e 100644 --- a/.github/workflows/check-image.yml +++ b/.github/workflows/check-image.yml @@ -9,14 +9,14 @@ jobs: - name: Checkout repo content uses: actions/checkout@v2 - name: ooil version - uses: docker://itisfoundation/ci-service-integration-library:v1.0.3-dev-4 + uses: docker://itisfoundation/ci-service-integration-library:v1.0.3-dev-8 with: args: ooil --version - name: Assemble docker-compose spec - uses: docker://itisfoundation/ci-service-integration-library:v1.0.3-dev-4 + uses: docker://itisfoundation/ci-service-integration-library:v1.0.3-dev-8 with: args: ooil compose - name: Build all images if multiple - uses: docker://itisfoundation/ci-service-integration-library:v1.0.3-dev-4 + uses: docker://itisfoundation/ci-service-integration-library:v1.0.3-dev-8 with: args: docker-compose build diff --git a/.osparc/jupyter-math/runtime.yml b/.osparc/jupyter-math/runtime.yml index 6f83724..32e020a 100644 --- a/.osparc/jupyter-math/runtime.yml +++ b/.osparc/jupyter-math/runtime.yml @@ -18,3 +18,8 @@ paths-mapping: outputs_path: /home/jovyan/work/outputs state_paths: - /home/jovyan/work/workspace +callbacks-mapping: + inactivity: + service: container + command: "/path/to/your/inactivity/hook" + timeout: 1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 40bfcc9..82bf24e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,8 +74,8 @@ RUN jupyter serverextension enable voila && \ # Import matplotlib the first time to build the font cache. ENV XDG_CACHE_HOME /home/$NB_USER/.cache/ RUN MPLBACKEND=Agg .venv/bin/python -c "import matplotlib.pyplot" && \ -fix-permissions /home/$NB_USER - # run fix permissions only once. This can be probably optimized, so it is faster to build + fix-permissions /home/$NB_USER +# run fix permissions only once. This can be probably optimized, so it is faster to build # copy README and CHANGELOG COPY --chown=$NB_UID:$NB_GID CHANGELOG.md ${NOTEBOOK_BASE_DIR}/CHANGELOG.md @@ -91,6 +91,9 @@ ENV JP_LSP_VIRTUAL_DIR="/home/${NB_USER}/.virtual_documents" # Copying boot scripts COPY --chown=$NB_UID:$NB_GID docker /docker +RUN chmod +x /docker/inactivity.py \ + && chmod +x /docker/kernel_cehcker.py + RUN echo 'export PATH="/home/${NB_USER}/.venv/bin:$PATH"' >> "/home/${NB_USER}/.bashrc" EXPOSE 8888 diff --git a/Makefile b/Makefile index cf4b223..3e5d796 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ define _bumpversion # upgrades as $(subst $(1),,$@) version, commits and tags @docker run -it --rm -v $(PWD):/${DOCKER_IMAGE_NAME} \ -u $(shell id -u):$(shell id -g) \ - itisfoundation/ci-service-integration-library:v1.0.3-dev-4 \ + itisfoundation/ci-service-integration-library:v1.0.3-dev-8 \ sh -c "cd /${DOCKER_IMAGE_NAME} && bump2version --verbose --list --config-file $(1) $(subst $(2),,$@)" endef @@ -49,7 +49,7 @@ version-patch version-minor version-major: .bumpversion.cfg ## increases service compose-spec: ## runs ooil to assemble the docker-compose.yml file @docker run -it --rm -v $(PWD):/${DOCKER_IMAGE_NAME} \ -u $(shell id -u):$(shell id -g) \ - itisfoundation/ci-service-integration-library:v1.0.3-dev-4 \ + itisfoundation/ci-service-integration-library:v1.0.3-dev-8 \ sh -c "cd /${DOCKER_IMAGE_NAME} && ooil compose" build: | compose-spec ## build docker image diff --git a/docker/entrypoint.bash b/docker/entrypoint.bash index 790a3d7..c36537a 100755 --- a/docker/entrypoint.bash +++ b/docker/entrypoint.bash @@ -74,4 +74,5 @@ chmod gu-w "/home/${NB_USER}/work" echo echo "$INFO" "Starting notebook ..." +exec gosu "$NB_USER" /docker/kernel_cehcker.py & exec gosu "$NB_USER" /docker/boot_notebook.bash diff --git a/docker/inactivity.py b/docker/inactivity.py new file mode 100644 index 0000000..c144d73 --- /dev/null +++ b/docker/inactivity.py @@ -0,0 +1,9 @@ + +#!/home/jovyan/.venv/bin/python + +# prints the result of the inactivity command + +import requests + +r = requests.get("http://localhost:9000") +print(r.text) \ No newline at end of file diff --git a/docker/kernel_cehcker.py b/docker/kernel_cehcker.py new file mode 100644 index 0000000..198cb66 --- /dev/null +++ b/docker/kernel_cehcker.py @@ -0,0 +1,90 @@ +#!/home/jovyan/.venv/bin/python + + +import asyncio +import json +import requests +from datetime import datetime +import tornado +from contextlib import suppress +from typing import Final + + +KERNEL_BUSY_CHECK_INTERVAL_S: Final[float] = 5 + + +class JupyterKernelChecker: + BASE_URL = "http://localhost:8888" + HEADERS = {"accept": "application/json"} + + def __init__(self) -> None: + self.last_busy: datetime| None = None + + def _get(self, path: str) -> dict: + r = requests.get(f'{self.BASE_URL}{path}', headers=self.HEADERS) + return r.json() + + def _are_kernels_busy(self)-> bool: + json_response = self._get("/api/kernels") + + are_kernels_busy = False + + for kernel_data in json_response: + kernel_id = kernel_data["id"] + + kernel_info = self._get(f"/api/kernels/{kernel_id}") + if kernel_info["execution_state"] != "idle": + are_kernels_busy = True + + return are_kernels_busy + + def check(self): + are_kernels_busy = self._are_kernels_busy() + print(f"{are_kernels_busy=}") + + if not are_kernels_busy: + self.last_busy = None + + if are_kernels_busy and self.last_busy is None: + self.last_busy = datetime.utcnow() + + + def get_idle_seconds(self)-> float: + if self.last_busy is None: + return 0 + + return (datetime.utcnow() - self.last_busy).total_seconds() + + async def run(self): + while True: + with suppress(Exception): + self.check() + await asyncio.sleep(KERNEL_BUSY_CHECK_INTERVAL_S) + + + +kernel_checker = JupyterKernelChecker() + + +class MainHandler(tornado.web.RequestHandler): + def get(self): + idle_seconds = kernel_checker.get_idle_seconds() + response = ( + {"is_inactive": True, "seconds_inactive" : idle_seconds} + if idle_seconds > 0 else + {"is_inactive": False, "seconds_inactive" : None} + ) + self.write(json.dumps(response)) + + +def make_app()-> tornado.web.Application: + return tornado.web.Application([(r"/", MainHandler)]) + +async def main(): + app = make_app() + app.listen(9000) + asyncio.create_task(kernel_checker.run()) + await asyncio.Event().wait() + +if __name__ == "__main__": + asyncio.run(main())