Metadata-Version: 2.3
Name: tableflip
Version: 0.1.0
Summary: Zero-downtime process upgrades for Python, inspired by cloudflare/tableflip
Author: Bernardo Vale
Author-email: Bernardo Vale <bernardo@kentik.com>
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# tableflip — Graceful process restarts in Python

Zero-downtime upgrades for Python network services. Update running code or configuration without dropping existing connections.

This is a Python port of Cloudflare's [tableflip](https://github.com/cloudflare/tableflip) Go library. The core design — fd inheritance, IPC protocol, and state machine — follows the original closely, adapted to Python's `asyncio` runtime.

**Works on Linux and macOS.** Raises `NotSupportedError` on Windows (use `tableflip.testing` stubs instead).

## How it works

1. On `SIGHUP`, the running process spawns a new copy of itself
2. TCP listener sockets are passed to the new process via fd inheritance
3. The new process signals readiness after initialization
4. The old process stops accepting new connections and exits

Only one upgrade runs at a time. If the new process crashes during init, the old one keeps serving.

## Installation

```bash
uv add tableflip
# or
pip install tableflip
```

Requires Python 3.13+.

## Usage

```python
import asyncio
import signal
from tableflip import Upgrader, Options


async def main():
    upg = await Upgrader.new(Options(pid_file="/tmp/myapp.pid"))

    # Trigger upgrade on SIGHUP
    loop = asyncio.get_running_loop()
    loop.add_signal_handler(signal.SIGHUP, lambda: asyncio.create_task(do_upgrade(upg)))

    # Listen must be called before ready()
    sock = await upg.fds.listen("127.0.0.1", 8080)

    server = await asyncio.start_server(handle_conn, sock=sock)

    await upg.ready()

    # Block until an upgrade completes or stop() is called
    await upg.exit().wait()

    # Graceful shutdown
    server.close()
    await server.wait_closed()
    await upg.wait_for_parent()
    upg.stop()


async def do_upgrade(upg: Upgrader):
    try:
        await upg.upgrade()
    except Exception as e:
        print(f"Upgrade failed: {e}")


async def handle_conn(reader, writer):
    writer.write(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
    await writer.drain()
    writer.close()


asyncio.run(main())
```

Trigger an upgrade:

```bash
kill -HUP $(cat /tmp/myapp.pid)
```

## Integration with systemd

```ini
[Unit]
Description=My Python service

[Service]
ExecStart=/path/to/venv/bin/python app.py
ExecReload=/bin/kill -HUP $MAINPID
PIDFile=/tmp/myapp.pid
```

## Testing

Use the `tableflip.testing` module for unit tests or unsupported platforms:

```python
from tableflip.testing import Upgrader, Fds

upg = Upgrader()          # never upgrades, upgrade() raises NotSupportedError
await upg.ready()          # no-op
assert not upg.has_parent()
```

## API

| Method | Description |
|--------|-------------|
| `await Upgrader.new(opts)` | Create an upgrader (one per process) |
| `await upg.fds.listen(addr, port)` | Get an inherited or new TCP listener |
| `await upg.ready()` | Signal readiness, notify parent, write PID file |
| `await upg.upgrade()` | Spawn new process and wait for it to become ready |
| `upg.exit()` | Returns `asyncio.Event` set when the process should exit |
| `upg.stop()` | Prevent further upgrades and trigger exit |
| `await upg.wait_for_parent()` | Block until parent process exits |
| `upg.has_parent()` | `True` if spawned by a tableflip upgrade |

## Acknowledgments

This is a Python port of [cloudflare/tableflip](https://github.com/cloudflare/tableflip) by Cloudflare. The Go library was created by Lorenz Bauer and the Cloudflare team. All credit for the design and protocol goes to them.

## License

See [LICENSE](LICENSE) (BSD 3-Clause, same as the original Go library).
