diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e8c3f..9da4b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The format loosely follows Keep a Changelog. --- +## 0.4.1 — 2026-06-21 + +### Fixed +- Lease now collapses immediately when `fn()` raises an exception. Previously the lease stayed alive until `hard_ttl_ms` expired, causing callers in that window to see `execution_alive=True` instead of `uncertain=True`. + +### Added +- `sentinel.inspect(key)` — inspect the current state of any lease, returns an `InspectResult` with status, liveness, expiry times, and execution result. +- `sen` CLI tool — `sen inspect ` reads `DATABASE_URL` from environment or `.env` and prints lease state directly to the terminal. +- `sentinel.reconcile` — standalone `Reconcile` class exposed as an attribute on `Sentinel`. Replaces the previous pattern of reconciliation methods on `OnceResult`. + +### Changed +- `reconcile()`, `force_complete()`, and `reset()` no longer require `owner_id` and `fencing_token`. Keys are resolved internally; WHERE clause guards handle safety. +- Reconciliation is no longer exposed on `OnceResult`. Use `sentinel.reconcile` instead. + +--- + ## [0.4.0] - 2026 ### Added diff --git a/Docs/PHILOSOPHY.md b/Docs/PHILOSOPHY.md index 0b36233..69e5d0e 100644 --- a/Docs/PHILOSOPHY.md +++ b/Docs/PHILOSOPHY.md @@ -240,6 +240,29 @@ The `claimed` → `executing` transition is the important one. Before that bound --- +## Inspecting Execution State + +At any point you can inspect the current state of a key directly: + +```python +result = sentinel.inspect("payments:charge:123") + +if result is None: + print("No lease found for this key") +else: + print(result.status) # claimed, executing, completed, reconciling + print(result.lease_alive) # whether the lease is still valid + print(result.lease_expires_at) + print(result.hard_expires_at) + print(result.execution_result) # set once completed +``` + +This is useful for debugging uncertain outcomes, monitoring long-running executions, and building observability on top of Sentinel. + +The `sen` CLI wraps this directly — see the CLI section in the README. + +--- + ## Reconciliation When execution ends up in an uncertain state, Sentinel gives you explicit tools to resolve it rather than forcing a guess. @@ -252,41 +275,37 @@ if result.execution_alive: elif result.uncertain: # Execution truth could not be established. - # Reconciliation tools are exposed on this result. - - result.reconcile(...) # Changes state, required before using force_complete and reset - result.reset(...) # Resets the state, so work can be claimed again for retries - result.force_complete(...) # Marks the work as completed, response object should also be passed + # Use sentinel.reconcile to resolve it explicitly. else: return result.response ``` -Example: -```python -result = sentinel.once(...) +Reconciliation tools are exposed on `sentinel.reconcile`: +```python if result.uncertain: - result.reconcile() + sentinel.reconcile.reconcile(key) # Verify downstream system. payment_exists = check_payment() if payment_exists: - result.force_complete( - response={"ok": True} + sentinel.reconcile.force_complete( + key, + execution_result={"ok": True} ) else: - result.reset() + sentinel.reconcile.reset(key) ``` The typical reconciliation pattern: 1. Detect `result.uncertain` on a result -2. Use `reconcile` to start reconciliation -3. Check your downstream system (did the payment go through?) +2. Call `sentinel.reconcile.reconcile(key)` to enter recovery mode +3. Check your downstream system — did the payment go through? 4. If yes: `force_complete` with the known result -5. If no or unknown: `reset` and let it retry +5. If no or unknown: `reset` and let it be claimed again This is more work than a silent retry. It's also the only approach that doesn't risk charging a customer twice. diff --git a/README.md b/README.md index 3bdf2df..048916d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Sentinel's primary interface is `once()`, which coordinates execution across com pip install sentinel-coordination ``` +![Sentinel CLI](assets/demo.gif) + Requires Python 3.9+ and a PostgreSQL database. --- @@ -51,6 +53,27 @@ sentinel = Sentinel( --- +## CLI + +Sentinel ships with `sen`, a command-line tool for inspecting lease state directly from your terminal. + +![Sentinel CLI](assets/demo.gif) + +### Inspect a lease + +```bash +sen inspect +``` + +`sen` reads `DATABASE_URL` from your environment or a `.env` file automatically. + +```bash +export DATABASE_URL=postgresql://user:password@localhost/mydb +sen inspect +``` + +--- + ## The Once API `sentinel.once()` is the primary interface. Given a key and a function, it guarantees that function runs **at most once per key** across any number of competing workers and returns the cached result to anyone else who asks. @@ -90,6 +113,7 @@ if result.execution_alive: elif result.uncertain: # Execution truth could not be established. # Use reconciliation tooling if needed. + # Reconciallition tooling documentation is in Docs/philosophy.md else: # If execution_alive and uncertain are both False, diff --git a/assets/demo.gif b/assets/demo.gif new file mode 100644 index 0000000..da5dae9 Binary files /dev/null and b/assets/demo.gif differ diff --git a/sentinel-py/pyproject.toml b/sentinel-py/pyproject.toml index 6cb2db8..132ce70 100644 --- a/sentinel-py/pyproject.toml +++ b/sentinel-py/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sentinel-coordination" -version = "0.4.0" +version = "0.4.1" description = "PostgreSQL-backed execution coordination primitive for correctness-sensitive distributed work." authors = [ { name = "Sreejay Reddy", email = "reddysreejay@gmail.com" }