Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <key>` 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
Expand Down
49 changes: 34 additions & 15 deletions Docs/PHILOSOPHY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand Down Expand Up @@ -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 <key>
```

`sen` reads `DATABASE_URL` from your environment or a `.env` file automatically.

```bash
export DATABASE_URL=postgresql://user:password@localhost/mydb
sen inspect <key>
```

---

## 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.
Expand Down Expand Up @@ -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,
Expand Down
Binary file added assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion sentinel-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand Down
Loading