Skip to content

Commit

Permalink
Working watchdog
Browse files Browse the repository at this point in the history
  • Loading branch information
ajnisbet committed Oct 11, 2024
1 parent 9d51071 commit d1fc1a7
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 66 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ build-m1:
rebuild:
docker build --no-cache --tag opentopodata:$(VERSION) --file docker/Dockerfile .

rebuild-m1-no-cache:
docker build --tag opentopodata:$(VERSION) --file docker/apple-silicon.Dockerfile .
rebuild-m1:
docker build --no-cache --tag opentopodata:$(VERSION) --file docker/apple-silicon.Dockerfile .

run:
docker run --rm -it --volume "$(shell pwd)/data:/app/data:ro" -p 5000:5000 opentopodata:$(VERSION)
Expand All @@ -29,7 +29,7 @@ run-local:
FLASK_APP=opentopodata/api.py FLASK_DEBUG=1 flask run --port 5000

black:
black --target-version py311 tests opentopodata
black --target-version py311 tests opentopodata docker

black-check:
docker run --rm opentopodata:$(VERSION) python -m black --check --target-version py311 tests opentopodata
Expand Down
2 changes: 2 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ FROM python:3.11.10-slim-bookworm
RUN set -e && \
apt-get update && \
apt-get install -y --no-install-recommends \
inotify-tools \
nano \
nginx \
memcached \
supervisor && \
Expand Down
110 changes: 110 additions & 0 deletions docker/config_watcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import logging
import time
from pathlib import Path
import subprocess
import sys

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler


# Paths.
CONFIG_DIR = Path("/app/")
CONFIG_PATH = Path("/app/config.yaml")
EXAMPLE_CONFIG_PATH = Path("/app/example-config.yaml")

# Debouncing: once the config has been reloaded, any queued unprocessed events should be discarded.
LAST_INVOCATION_TIME = time.time()


# Logger setup.
logger = logging.getLogger("configwatcher")
LOG_FORMAT = "%(asctime)s %(levelname)-8s %(message)s"
formatter = logging.Formatter(LOG_FORMAT)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.INFO)
handler.setFormatter(formatter)
logger.addHandler(handler)


def run_cmd(cmd, shell=False):
r = subprocess.run(cmd, shell=shell, capture_output=True)
is_error = r.returncode != 0
stdout = r.stdout.decode("utf-8")
if is_error:
logger.error(f"Error running command, returncode: {r.returncode}")
logger.error("cmd:")
logger.error(" ".join(cmd))
if r.stdout:
logger.error("stdout:")
logger.error(stdout)
if r.stderr:
logger.error("stderr:")
logger.error(r.stderr.decode("utf-8"))
raise ValueError
return stdout


def reload_config():
global LAST_INVOCATION_TIME
LAST_INVOCATION_TIME = time.time()
logger.info("Restarting OTD due to config change.")
run_cmd(["supervisorctl", "-c", "/app/docker/supervisord.conf", "stop", "uwsgi"])
run_cmd(
["supervisorctl", "-c", "/app/docker/supervisord.conf", "restart", "memcached"]
)
run_cmd(["supervisorctl", "-c", "/app/docker/supervisord.conf", "start", "uwsgi"])
run_cmd(
["supervisorctl", "-c", "/app/docker/supervisord.conf", "start", "warm_cache"]
)
LAST_INVOCATION_TIME = time.time()
logger.info("Restarted OTD due to config change.")


class Handler(FileSystemEventHandler):

def on_any_event(self, event):
watch_paths_str = [
EXAMPLE_CONFIG_PATH.as_posix(),
CONFIG_PATH.as_posix(),
]

# Filter unwanted events.
if event.event_type not in {"modified", "created"}:
logger.info(f"Dropping event with type {event.event_type=}")
return
if event.is_directory:
logger.info(f"Dropping dir event")
return
if event.src_path not in watch_paths_str:
logger.info(f"Dropping event with path {event.src_path=}")
return
if not Path(event.src_path).exists():
logger.info(f"Dropping event for nonexistent path {event.src_path=}")
return

# Debouncing.
mtime = Path(event.src_path).lstat().st_mtime
if mtime < LAST_INVOCATION_TIME:
msg = f"Dropping event for file that hasn't been modified since the last run. {event.src_path=}"
logger.info(msg)
return

logger.info(f"Dispatching event on {event.src_path=}")
reload_config()


if __name__ == "__main__":

event_handler = Handler()
observer = Observer()
observer.schedule(event_handler, CONFIG_DIR, recursive=False)
observer.start()

try:
while True:
time.sleep(1)
finally:
observer.stop()
observer.join()
20 changes: 16 additions & 4 deletions docker/supervisord.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
nodaemon=true
user=root


# Supervisorctl config/
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock


# OTD services.

[program:uwsgi]
command=/usr/local/bin/uwsgi --ini /app/docker/uwsgi.ini --processes %(ENV_N_UWSGI_THREADS)s
stdout_logfile=/dev/stdout
Expand Down Expand Up @@ -33,10 +46,9 @@ stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=false

[program:watchdog]
command=python /app/docker/watcher.py
[program:watch_config]
command=python /app/docker/config_watcher.py
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

stderr_logfile_maxbytes=0
4 changes: 3 additions & 1 deletion docker/warm_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@
break

else:
logging.error("Timeout while trying to pre-populate the cache. This probably means that Open Topo Data isn't working.")
logging.error(
"Timeout while trying to pre-populate the cache. This probably means that Open Topo Data isn't working."
)
sys.exit(1)
48 changes: 0 additions & 48 deletions docker/watcher.py

This file was deleted.

2 changes: 1 addition & 1 deletion example-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# An example of a config.yaml file showing all possible options. If no
# config.yaml file exists, opentopodata will load example-config.yaml instead.
# config.yaml file exists, opentopodata will load example-config.yaml instead..


# 400 error will be thrown above this limit.
Expand Down
3 changes: 1 addition & 2 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ pytest-timeout
PyYAML
rasterio>=1.3.8,<1.4.0 # 1.3.8+ avoids memory leak https://github.com/ajnisbet/opentopodata/issues/68; 1.4.0 introduces some bugs in rowcol/xy (as of 2024-10-11).
requests
watchdog # Monitor changes on the config.yaml
pymemcache # Handle cache flushing without messing
watchdog
11 changes: 4 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,10 @@ polyline==2.0.2
# via -r requirements.in
pylibmc==1.6.3
# via -r requirements.in

pymemcache==4.0.0
# via -r requirements.in
pyparsing==3.1.4
# via rasterio
# via snuggs
pyproj==3.7.0
# via -r requirements.in

pyproject-hooks==1.2.0
# via
# build
Expand All @@ -104,15 +100,16 @@ pytest-timeout==2.3.1
# via -r requirements.in
pyyaml==6.0.2
# via -r requirements.in
rasterio==1.4.1
rasterio==1.3.11
# via -r requirements.in
requests==2.32.3
# via -r requirements.in
snuggs==1.4.7
# via rasterio
urllib3==2.2.3
# via requests
watchdog==5.0.3
# via -r requirements.in

werkzeug==3.0.4
# via flask
wheel==0.44.0
Expand Down

0 comments on commit d1fc1a7

Please sign in to comment.