Source code for oasislmf.utils.ping

import json
import websocket
import socket
import os
import logging
import requests
import threading
from oasislmf.utils.defaults import SERVER_DEFAULT_PORT, SERVER_DEFAULT_IP

[docs] logger = logging.getLogger(__name__)
# Guards `oasis_ping_async` so a stuck ping target can never pile up more than # one in-flight ping thread within this process. _ping_lock = threading.Lock()
[docs] def oasis_ping(data): """Sends a JSON message to either an HTTP endpoint, a websocket server, or a socket server. If `analysis_pk` is in the data, targets are tried in order until one succeeds: - if `OASIS_ANALYSIS_STATUS_URL` is in environment, POSTs the message to that URL. - if `OASIS_WEBSOCKET_URL` and `OASIS_WEBSOCKET_PORT` are in environment, sends a websocket message. - if neither is configured, or all configured targets fail, no message gets through. Else, a message sent to `OASIS_SOCKET_SERVER_IP` `OASIS_SOCKET_SERVER_PORT` defaulted to 127.0.0.1 8888. If ``data`` contains a ``port_override`` key, that port is used in place of the default/env-var port when connecting to the socket server. The key is stripped before the message is sent. For a specific target, use `oasis_ping_http`, `oasis_ping_socket` or `oasis_ping_websocket` directly. Args: data (dict): dictionary of data: JSON serialisable Returns: Boolean: whether attempted call gets through """ if data.get('analysis_pk', None) is not None: attempted = False if 'OASIS_ANALYSIS_STATUS_URL' in os.environ: attempted = True url = os.environ['OASIS_ANALYSIS_STATUS_URL'] logger.debug(f"Sending ping to {url}: {data}") if oasis_ping_http(url, data): return True if all(item in os.environ for item in ['OASIS_WEBSOCKET_URL', 'OASIS_WEBSOCKET_PORT']): attempted = True msg = json.dumps(data) ws_url = f"{os.environ['OASIS_WEBSOCKET_URL']}:{os.environ['OASIS_WEBSOCKET_PORT']}/ws/analysis-status/" logger.debug(f"Sending ping to {ws_url}: {msg}") if oasis_ping_websocket(ws_url, msg): return True if not attempted: logger.error("Missing environment variables `OASIS_ANALYSIS_STATUS_URL` or " "`OASIS_WEBSOCKET_URL`/`OASIS_WEBSOCKET_PORT`.") return False port_override = data.pop('port_override', None) msg = json.dumps(data) target_port = int(port_override) if port_override is not None else int(os.environ.get("OASIS_SOCKET_SERVER_PORT", SERVER_DEFAULT_PORT)) target = (os.environ.get("OASIS_SOCKET_SERVER_IP", SERVER_DEFAULT_IP), target_port) logger.debug(f"Sending ping to {target}: {msg}") return oasis_ping_socket(target, msg)
[docs] def oasis_ping_async(data): """Sends a ping without blocking the caller. `oasis_ping` makes a network call (HTTP POST or websocket connect) that can block for up to the configured timeout when the target is unreachable but not actively refusing the connection (wrong hostname, dropped packets, a NetworkPolicy). Calling it directly from a per-event progress ping inside a compute loop means a broken ping target stalls the calculation itself. This runs `oasis_ping` on a background daemon thread instead, so a stuck ping can never block the caller. Only one ping is ever in flight at a time per process: if a previous call is still pending when this is invoked, the new update is dropped (a subsequent ping will carry a more up to date `events_complete`) rather than letting a persistently broken target pile up threads. Args: data (dict): dictionary of data: JSON serialisable """ if not _ping_lock.acquire(blocking=False): logger.debug(f"Skipping ping, previous ping still in flight: {data}") return def _send(): try: oasis_ping(data) finally: _ping_lock.release() threading.Thread(target=_send, daemon=True).start()
[docs] def oasis_ping_socket(target, data): """Sends a JSON message to a target socket Args: target ((str, int)): IP and port to hit data (str): JSON dumped string Returns: Boolean: whether attempted call gets through """ try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as oasis_socket: oasis_socket.connect(target) oasis_socket.sendall(data.encode('utf-8')) return True except (ConnectionError, TimeoutError, socket.gaierror) as e: logger.error(f"oasis_ping_socket could not connect: {e}") return False
[docs] def oasis_ping_http(url, data): """Sends a JSON message to a target HTTP endpoint via POST. Args: url (str): URL to hit (e.g. "http://oasis-server:8000/analysis-status/") data (dict): dictionary of data: JSON serialisable Returns: Boolean: whether attempted call gets through """ try: response = requests.post(url, json=data, timeout=1) response.raise_for_status() return True except requests.exceptions.RequestException as e: logger.error(f"oasis_ping_http could not connect: {e}") return False
[docs] def oasis_ping_websocket(ws_url, data): """Sends a JSON message to a target websocket Args: ws_url (str): URL to hit (e.g. "ws://oasis-websocket:8001/ws/analysis-status/") data (str): JSON dumped string Returns: Boolean: whether attempted call gets through """ ws = websocket.WebSocket() try: ws.connect(ws_url, timeout=1) ws.send(data) return True except Exception as e: logger.error(f"oasis_ping_websocket could not connect: {e}") return False finally: ws.close()