POMDPPlanners.simulations.simulations_deployment.run_progress package
Progress tracking + notification module for long experiment runs.
Two layers:
Layer 1 (in-process): a
Notifierconstructed automatically byBaseSimulator. Emitsrun_started/episode_completed(heartbeat) /run_finished/run_failedevents to a local SQLite DB and (if a Slack webhook is configured) to Slack.Layer 2 (external watcher): a CLI module (
POMDPPlanners.simulations.simulations_deployment.run_progress.watcher) that reads the same DB and emitsrun_stalledSlack notifications for runs whose last heartbeat is older than a configurable threshold. Run from cron to catch hard process death (SIGKILL / OOM / reboot).
Whether notifications are sent is controlled by the deployment environment:
build_notifier() inspects SLACK_WEBHOOK_URL and a few opt-out env
vars. User-facing code constructs SimulationsAPI as usual; no
configuration is threaded through the public API.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.NotificationConfig(webhook_url=None, trial_interval=0, progress_db_path=None, disable=False)[source]
Bases:
objectConfiguration for Slack + progress-DB notifications.
Construct directly for programmatic per-instance control, or via
from_env()to mirror the legacy env-var-driven behaviour at the API layer.- Parameters:
- webhook_url
Slack incoming-webhook URL.
Nonemeans “no Slack posts” (events still write to the progress DB if a notifier is constructed, but the SlackNotifier is replaced with a NullNotifier).
- trial_interval
Number of completed Optuna trials between milestone Slack messages during hyperparameter tuning.
0disables milestone messages. Only theHyperParameterOptimizerreads this; the simulator ignores it.
- progress_db_path
Optional override for the SQLite progress DB path.
Nonefalls back to the path resolved byProgressDB(POMDP_PROGRESS_DBenv var, then~/.cache/POMDPPlanners/progress.db).
- disable
Hard kill switch. When
True, the notifier built from this config is always aNullNotifier, even ifwebhook_urlis set. Equivalent to the legacyPOMDPPLANNERS_DISABLE_NOTIFY=1env var.
Example
Enabling Slack notifications has two equivalent paths.
Zero-config (env var). Export
SLACK_WEBHOOK_URLand anySimulationsAPIconstructed without an explicitnotification_configwill pick it up automatically viafrom_env():# In the shell: # export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..." from POMDPPlanners.simulations.simulation_apis.local_simulations_api \ import LocalSimulationsAPI api = LocalSimulationsAPI() # notifications enabled
Other env vars read by
from_env():POMDPPLANNERS_DISABLE_NOTIFY=1(silence a single invocation even if a webhook is set),HYPERPARAM_TRIAL_NOTIFICATION_INTERVAL(Optuna milestone cadence; default 50, set0to disable),POMDP_PROGRESS_DB(override the SQLite progress DB path). Pytest runs are auto-silenced viaPYTEST_CURRENT_TEST.Programmatic (per-instance control). Construct explicitly to route multiple simulations in the same process to different Slack channels, or to override env-driven defaults:
from POMDPPlanners.simulations.simulations_deployment.run_progress \ import NotificationConfig cfg_a = NotificationConfig(webhook_url="https://hooks.slack.com/A") cfg_b = NotificationConfig(webhook_url="https://hooks.slack.com/B") api_a = LocalSimulationsAPI(notification_config=cfg_a) api_b = LocalSimulationsAPI(notification_config=cfg_b)
Stall detection (external watcher). The in-process notifier cannot report a death the process itself does not witness (SIGKILL / OOM / host reboot). Run this once a minute from cron to catch those:
* * * * * /path/to/.venv/bin/python -m \ POMDPPlanners.simulations.simulations_deployment.run_progress.watcher \ --threshold-seconds 3600
--threshold-secondsis the heartbeat age above which a still- running entry is reported as stalled.- classmethod disabled()[source]
Return a config with notifications hard-disabled.
Used as the default for simulation classes constructed without an explicit config (e.g. unit tests, workflows called outside a
SimulationsAPI).- Return type:
- Returns:
A
NotificationConfigwithdisable=True.
- classmethod from_env()[source]
Build a config from the run-progress environment variables.
Reads
SLACK_WEBHOOK_URL,POMDPPLANNERS_DISABLE_NOTIFY,PYTEST_CURRENT_TEST,HYPERPARAM_TRIAL_NOTIFICATION_INTERVAL, andPOMDP_PROGRESS_DB. Returns a config that is functionally disabled (disable=True) when running under pytest or whenPOMDPPLANNERS_DISABLE_NOTIFY=1is set; otherwise reflects the webhook URL (orNoneif unset) and the milestone interval (default 50).- Return type:
- Returns:
A new
NotificationConfiginstance.
- is_active()[source]
Return
Trueiff this config should produce a real Slack notifier.A config is active when it is not hard-disabled and has a non-empty webhook URL. When
False,build_notifier()returns aNullNotifier.- Return type:
- class POMDPPlanners.simulations.simulations_deployment.run_progress.Notifier(*args, **kwargs)[source]
Bases:
ProtocolProtocol describing the four run-lifecycle events a simulator emits.
Implementations may write to a database, post to a chat tool, both, or neither. The Protocol is
runtime_checkable()so tests can assert structural conformance without inheritance.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.NullNotifier[source]
Bases:
objectNo-op
Notifier. All methods discard their arguments.Used when notifications are disabled (
SLACK_WEBHOOK_URLunset,POMDPPLANNERS_DISABLE_NOTIFY=1set, or running under pytest).Example
Basic usage:
notifier = NullNotifier() notifier.run_started("rid", metadata={"k": "v"}) notifier.episode_completed("rid") notifier.run_finished("rid")
- class POMDPPlanners.simulations.simulations_deployment.run_progress.ProgressDB(path=None)[source]
Bases:
objectSQLite-backed progress writer / reader for experiment runs.
Opens a fresh connection per call (no shared connection state), so each method is safe to call from any thread inside the parent process. WAL journaling is enabled on first connect so that a separate watcher process can read concurrently. The path can be supplied explicitly or resolved from the
POMDP_PROGRESS_DBenvironment variable; if neither is set, the default is~/.cache/POMDPPlanners/progress.db.Example
Basic usage:
db = ProgressDB() db.start_run("abc123", "tiger_experiment", {"episodes": 100}) db.heartbeat("abc123") db.finish_run("abc123", status="finished", error=None)
- Parameters:
path (Path | None)
- list_stalled(threshold_seconds)[source]
Return runs whose last heartbeat is older than
threshold_seconds.Only
status='running'rows that have not yet been stall-notified are returned, so the watcher can use this list directly without additional filtering.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.RunRow(run_id, experiment_name, host, pid, status, started_at, finished_at, last_heartbeat_at, error_msg, metadata, stall_notified_at)[source]
Bases:
objectA snapshot of one row in the
runstable.- Parameters:
- run_id
Unique id for the run (typically
uuid4().hex).
- experiment_name
Human-readable label for the run (e.g. the simulator experiment name).
- host
Host the run started on, as returned by
socket.gethostname().
- pid
Process id of the parent simulator process.
- status
One of
"running","finished", or"failed".
- started_at
ISO-8601 UTC timestamp of run start.
- finished_at
ISO-8601 UTC timestamp of run finish, or
Nonewhile running.
- last_heartbeat_at
ISO-8601 UTC timestamp of the most recent heartbeat.
- error_msg
Stringified exception if the run failed, else
None.
- metadata
JSON-decoded metadata dict supplied at run start.
- stall_notified_at
ISO-8601 UTC timestamp at which a watcher sent a stall notification for this run, or
Noneif no stall notification has been sent.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.SlackNotifier(webhook_url, experiment_name, *, db=None, logger=None)[source]
Bases:
objectNotifierthat writes toProgressDBand Slack.run_started/run_finished/run_failedwrite to the DB and POST a short message to the Slack webhook.episode_completedonly writes a heartbeat to the DB — it does not post to Slack, to avoid spamming the channel during long runs.Slack-side failures are caught and logged at WARNING level via the supplied logger; they do not propagate to the caller.
Example
Basic usage:
notifier = SlackNotifier( webhook_url="https://hooks.slack.com/services/T/B/X", experiment_name="tiger_experiment", ) notifier.run_started("rid-1", metadata={"episodes": 100}) notifier.episode_completed("rid-1") notifier.run_finished("rid-1")
- Parameters:
webhook_url (str)
experiment_name (str)
db (ProgressDB | None)
logger (logging.Logger | None)
- POMDPPlanners.simulations.simulations_deployment.run_progress.build_notifier(experiment_name, *, config, logger=None)[source]
Construct the appropriate
Notifierfrom a config.This function performs no environment reads. The caller passes an explicit
NotificationConfig; if that config is active (non-empty webhook URL ANDdisable=False), aSlackNotifieris returned. Otherwise aNullNotifieris returned. Env-var resolution belongs inNotificationConfig.from_env()— typically invoked by theSimulationsAPIlayer.- Parameters:
experiment_name (
str) – Human-readable label used for run records and Slack messages.config (
NotificationConfig) – TheNotificationConfigcontrolling whether and where Slack messages are sent.logger (
Logger|None) – Optional logger forwarded toSlackNotifier.
- Return type:
- Returns:
A
Notifierinstance — eitherNullNotifierorSlackNotifierdepending on whetherconfig.is_active().
- POMDPPlanners.simulations.simulations_deployment.run_progress.install_signal_handlers(notifier, run_id)[source]
Install SIGTERM/SIGINT handlers that emit
Notifier.run_failed().The installed handlers:
Call
notifier.run_failed(run_id, error="signal:<NAME>"). Exceptions inside the notifier are swallowed.Chain to the previous handler if it is callable. Non-callable handlers (
signal.SIG_DFL/signal.SIG_IGN, both integer sentinels) are skipped, and the signal is re-delivered to the process with default handling restored.
Returns a callable that uninstalls both handlers (restoring whatever was previously installed). Calling the uninstall function multiple times is safe.
- Parameters:
- Return type:
- Returns:
An idempotent uninstall callable. The caller should invoke it during normal teardown (e.g.
BaseSimulator.__exit__) so signal handling reverts to the prior configuration.
- POMDPPlanners.simulations.simulations_deployment.run_progress.post_trial_milestone(webhook_url, *, experiment_name, run_id, completed_trials, total_trials, config_name, best_score, logger=None)[source]
POST a Slack message reporting hyperparameter-tuning progress.
Stateless helper called once per
HYPERPARAM_TRIAL_NOTIFICATION_INTERVALOptuna trials.webhook_urlmust be a non-empty Slack incoming-webhook URL — callers (typically the in-task Optuna callback) are responsible for skipping the call when notifications are disabled. Network errors are caught and logged at WARNING level; nothing propagates to the caller.- Parameters:
webhook_url (
str) – Slack incoming-webhook URL.experiment_name (
str) – Human-readable label for the tuning run, included in the message body.run_id (
str) – Unique id of the parent tuning run (same id used byrun_started/run_finished).completed_trials (
int) – Number of trials completed globally so far.total_trials (
int) – Total trials scheduled across all configs.config_name (
str) – Name of the config whose trial just completed; lets the reader see which leg of the sweep is currently running.best_score (
float|None) – Best objective value seen so far in the current config (e.g.study.best_value). PassNoneif no trial has produced a usable score yet — the message will note this.logger (
Logger|None) – Optional logger for swallowed-error WARNING. Defaults to a logger namedrun_progress.notify.
- Return type:
Submodules
POMDPPlanners.simulations.simulations_deployment.run_progress.config module
Notification configuration dataclass for the run_progress module.
NotificationConfig bundles every parameter the run-progress
infrastructure needs into a single explicit object. Simulation classes
(BaseSimulator, HyperParameterOptimizer) accept one as a constructor
argument; the SimulationsAPI layer is the only thing that reads env
vars (via NotificationConfig.from_env()).
This split keeps simulation classes pure DI — they no longer touch
os.environ at construction time — while preserving the existing
zero-config UX through the API layer.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.config.NotificationConfig(webhook_url=None, trial_interval=0, progress_db_path=None, disable=False)[source]
Bases:
objectConfiguration for Slack + progress-DB notifications.
Construct directly for programmatic per-instance control, or via
from_env()to mirror the legacy env-var-driven behaviour at the API layer.- Parameters:
- webhook_url
Slack incoming-webhook URL.
Nonemeans “no Slack posts” (events still write to the progress DB if a notifier is constructed, but the SlackNotifier is replaced with a NullNotifier).
- trial_interval
Number of completed Optuna trials between milestone Slack messages during hyperparameter tuning.
0disables milestone messages. Only theHyperParameterOptimizerreads this; the simulator ignores it.
- progress_db_path
Optional override for the SQLite progress DB path.
Nonefalls back to the path resolved byProgressDB(POMDP_PROGRESS_DBenv var, then~/.cache/POMDPPlanners/progress.db).
- disable
Hard kill switch. When
True, the notifier built from this config is always aNullNotifier, even ifwebhook_urlis set. Equivalent to the legacyPOMDPPLANNERS_DISABLE_NOTIFY=1env var.
Example
Enabling Slack notifications has two equivalent paths.
Zero-config (env var). Export
SLACK_WEBHOOK_URLand anySimulationsAPIconstructed without an explicitnotification_configwill pick it up automatically viafrom_env():# In the shell: # export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..." from POMDPPlanners.simulations.simulation_apis.local_simulations_api \ import LocalSimulationsAPI api = LocalSimulationsAPI() # notifications enabled
Other env vars read by
from_env():POMDPPLANNERS_DISABLE_NOTIFY=1(silence a single invocation even if a webhook is set),HYPERPARAM_TRIAL_NOTIFICATION_INTERVAL(Optuna milestone cadence; default 50, set0to disable),POMDP_PROGRESS_DB(override the SQLite progress DB path). Pytest runs are auto-silenced viaPYTEST_CURRENT_TEST.Programmatic (per-instance control). Construct explicitly to route multiple simulations in the same process to different Slack channels, or to override env-driven defaults:
from POMDPPlanners.simulations.simulations_deployment.run_progress \ import NotificationConfig cfg_a = NotificationConfig(webhook_url="https://hooks.slack.com/A") cfg_b = NotificationConfig(webhook_url="https://hooks.slack.com/B") api_a = LocalSimulationsAPI(notification_config=cfg_a) api_b = LocalSimulationsAPI(notification_config=cfg_b)
Stall detection (external watcher). The in-process notifier cannot report a death the process itself does not witness (SIGKILL / OOM / host reboot). Run this once a minute from cron to catch those:
* * * * * /path/to/.venv/bin/python -m \ POMDPPlanners.simulations.simulations_deployment.run_progress.watcher \ --threshold-seconds 3600
--threshold-secondsis the heartbeat age above which a still- running entry is reported as stalled.- classmethod disabled()[source]
Return a config with notifications hard-disabled.
Used as the default for simulation classes constructed without an explicit config (e.g. unit tests, workflows called outside a
SimulationsAPI).- Return type:
- Returns:
A
NotificationConfigwithdisable=True.
- classmethod from_env()[source]
Build a config from the run-progress environment variables.
Reads
SLACK_WEBHOOK_URL,POMDPPLANNERS_DISABLE_NOTIFY,PYTEST_CURRENT_TEST,HYPERPARAM_TRIAL_NOTIFICATION_INTERVAL, andPOMDP_PROGRESS_DB. Returns a config that is functionally disabled (disable=True) when running under pytest or whenPOMDPPLANNERS_DISABLE_NOTIFY=1is set; otherwise reflects the webhook URL (orNoneif unset) and the milestone interval (default 50).- Return type:
- Returns:
A new
NotificationConfiginstance.
POMDPPlanners.simulations.simulations_deployment.run_progress.db module
SQLite-backed progress storage for long experiment runs.
Stores per-run metadata, status transitions, and a heartbeat timestamp that gets bumped once per completed episode. The DB is written to only by the parent process of a run (joblib workers fork, and SQLite connections do not survive fork). Reads from a separate watcher process are safe thanks to WAL mode plus a 5-second busy timeout.
- Classes:
ProgressDB: SQLite progress writer / reader. RunRow: Frozen dataclass mirroring one row of the
runstable.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.db.ProgressDB(path=None)[source]
Bases:
objectSQLite-backed progress writer / reader for experiment runs.
Opens a fresh connection per call (no shared connection state), so each method is safe to call from any thread inside the parent process. WAL journaling is enabled on first connect so that a separate watcher process can read concurrently. The path can be supplied explicitly or resolved from the
POMDP_PROGRESS_DBenvironment variable; if neither is set, the default is~/.cache/POMDPPlanners/progress.db.Example
Basic usage:
db = ProgressDB() db.start_run("abc123", "tiger_experiment", {"episodes": 100}) db.heartbeat("abc123") db.finish_run("abc123", status="finished", error=None)
- Parameters:
path (Path | None)
- list_stalled(threshold_seconds)[source]
Return runs whose last heartbeat is older than
threshold_seconds.Only
status='running'rows that have not yet been stall-notified are returned, so the watcher can use this list directly without additional filtering.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.db.RunRow(run_id, experiment_name, host, pid, status, started_at, finished_at, last_heartbeat_at, error_msg, metadata, stall_notified_at)[source]
Bases:
objectA snapshot of one row in the
runstable.- Parameters:
- run_id
Unique id for the run (typically
uuid4().hex).
- experiment_name
Human-readable label for the run (e.g. the simulator experiment name).
- host
Host the run started on, as returned by
socket.gethostname().
- pid
Process id of the parent simulator process.
- status
One of
"running","finished", or"failed".
- started_at
ISO-8601 UTC timestamp of run start.
- finished_at
ISO-8601 UTC timestamp of run finish, or
Nonewhile running.
- last_heartbeat_at
ISO-8601 UTC timestamp of the most recent heartbeat.
- error_msg
Stringified exception if the run failed, else
None.
- metadata
JSON-decoded metadata dict supplied at run start.
- stall_notified_at
ISO-8601 UTC timestamp at which a watcher sent a stall notification for this run, or
Noneif no stall notification has been sent.
POMDPPlanners.simulations.simulations_deployment.run_progress.notify module
Notifier interface, implementations, and signal-handler installer.
The Notifier Protocol defines four events that the simulator emits
during a run:
run_started— fired at the entry point of the experiment loop.episode_completed— fired once per completed episode (heartbeat).run_finished— fired on a clean exit.run_failed— fired when an exception or signal aborts the run.
Two concrete implementations are provided:
NullNotifier— no-op; used when notifications are disabled.SlackNotifier— writes events to the local progress DB and posts to a Slack incoming webhook (via stdliburllib.request).
The build_notifier() factory consumes a NotificationConfig
and returns the appropriate implementation; it does not read any
environment variables itself — env-var resolution is the responsibility of
the caller (typically NotificationConfig.from_env() invoked by the
SimulationsAPI layer). The install_signal_handlers() helper
attaches SIGTERM/SIGINT handlers that emit run_failed before re-raising.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.notify.Notifier(*args, **kwargs)[source]
Bases:
ProtocolProtocol describing the four run-lifecycle events a simulator emits.
Implementations may write to a database, post to a chat tool, both, or neither. The Protocol is
runtime_checkable()so tests can assert structural conformance without inheritance.
- class POMDPPlanners.simulations.simulations_deployment.run_progress.notify.NullNotifier[source]
Bases:
objectNo-op
Notifier. All methods discard their arguments.Used when notifications are disabled (
SLACK_WEBHOOK_URLunset,POMDPPLANNERS_DISABLE_NOTIFY=1set, or running under pytest).Example
Basic usage:
notifier = NullNotifier() notifier.run_started("rid", metadata={"k": "v"}) notifier.episode_completed("rid") notifier.run_finished("rid")
- class POMDPPlanners.simulations.simulations_deployment.run_progress.notify.SlackNotifier(webhook_url, experiment_name, *, db=None, logger=None)[source]
Bases:
objectNotifierthat writes toProgressDBand Slack.run_started/run_finished/run_failedwrite to the DB and POST a short message to the Slack webhook.episode_completedonly writes a heartbeat to the DB — it does not post to Slack, to avoid spamming the channel during long runs.Slack-side failures are caught and logged at WARNING level via the supplied logger; they do not propagate to the caller.
Example
Basic usage:
notifier = SlackNotifier( webhook_url="https://hooks.slack.com/services/T/B/X", experiment_name="tiger_experiment", ) notifier.run_started("rid-1", metadata={"episodes": 100}) notifier.episode_completed("rid-1") notifier.run_finished("rid-1")
- Parameters:
webhook_url (str)
experiment_name (str)
db (ProgressDB | None)
logger (logging.Logger | None)
- POMDPPlanners.simulations.simulations_deployment.run_progress.notify.build_notifier(experiment_name, *, config, logger=None)[source]
Construct the appropriate
Notifierfrom a config.This function performs no environment reads. The caller passes an explicit
NotificationConfig; if that config is active (non-empty webhook URL ANDdisable=False), aSlackNotifieris returned. Otherwise aNullNotifieris returned. Env-var resolution belongs inNotificationConfig.from_env()— typically invoked by theSimulationsAPIlayer.- Parameters:
experiment_name (
str) – Human-readable label used for run records and Slack messages.config (
NotificationConfig) – TheNotificationConfigcontrolling whether and where Slack messages are sent.logger (
Logger|None) – Optional logger forwarded toSlackNotifier.
- Return type:
- Returns:
A
Notifierinstance — eitherNullNotifierorSlackNotifierdepending on whetherconfig.is_active().
- POMDPPlanners.simulations.simulations_deployment.run_progress.notify.install_signal_handlers(notifier, run_id)[source]
Install SIGTERM/SIGINT handlers that emit
Notifier.run_failed().The installed handlers:
Call
notifier.run_failed(run_id, error="signal:<NAME>"). Exceptions inside the notifier are swallowed.Chain to the previous handler if it is callable. Non-callable handlers (
signal.SIG_DFL/signal.SIG_IGN, both integer sentinels) are skipped, and the signal is re-delivered to the process with default handling restored.
Returns a callable that uninstalls both handlers (restoring whatever was previously installed). Calling the uninstall function multiple times is safe.
- Parameters:
- Return type:
- Returns:
An idempotent uninstall callable. The caller should invoke it during normal teardown (e.g.
BaseSimulator.__exit__) so signal handling reverts to the prior configuration.
- POMDPPlanners.simulations.simulations_deployment.run_progress.notify.post_trial_milestone(webhook_url, *, experiment_name, run_id, completed_trials, total_trials, config_name, best_score, logger=None)[source]
POST a Slack message reporting hyperparameter-tuning progress.
Stateless helper called once per
HYPERPARAM_TRIAL_NOTIFICATION_INTERVALOptuna trials.webhook_urlmust be a non-empty Slack incoming-webhook URL — callers (typically the in-task Optuna callback) are responsible for skipping the call when notifications are disabled. Network errors are caught and logged at WARNING level; nothing propagates to the caller.- Parameters:
webhook_url (
str) – Slack incoming-webhook URL.experiment_name (
str) – Human-readable label for the tuning run, included in the message body.run_id (
str) – Unique id of the parent tuning run (same id used byrun_started/run_finished).completed_trials (
int) – Number of trials completed globally so far.total_trials (
int) – Total trials scheduled across all configs.config_name (
str) – Name of the config whose trial just completed; lets the reader see which leg of the sweep is currently running.best_score (
float|None) – Best objective value seen so far in the current config (e.g.study.best_value). PassNoneif no trial has produced a usable score yet — the message will note this.logger (
Logger|None) – Optional logger for swallowed-error WARNING. Defaults to a logger namedrun_progress.notify.
- Return type:
POMDPPlanners.simulations.simulations_deployment.run_progress.watcher module
CLI watcher that posts Slack alerts for stalled experiment runs.
Intended to be invoked from cron (typically once per minute):
* * * * * /path/to/.venv/bin/python -m POMDPPlanners.simulations.simulations_deployment.run_progress.watcher --threshold-seconds 3600
For every run in the progress DB whose status is running AND whose
last_heartbeat_at is older than --threshold-seconds AND which has
not previously been stall-notified, the watcher POSTs a short Slack
message and marks the row notified so subsequent ticks do not duplicate.
The watcher is stateless — it makes one pass through the DB and exits. Cron handles “the watcher died” semantics so no separate daemon is needed.