Skip to main content

Pinned

Durable Objects for Python.

A PinnedAPI instance for a given id will exist exactly once in your fleet. It acts as a singleton webserver.

PinnedAPI

from pinned import PinnedAPI, route

class MyAPI(PinnedAPI):
    async def lifespan(self):
        # setup
        yield
        # teardown

    @route.get("/hello")
    async def hello(self):
        return {"hello": self.id}
  • Handlers must be async def.
  • Ids are client-generated; the first request activates.
  • @route.get/post/websocket see the path remainder after the id.
  • self.create_task(coro) runs background work bound to the instance: it blocks idle eviction while running, teardown waits for it, abort() cancels it.

PinnedHost

from pinned import PinnedHost

app = FastAPI()
app.include_router(
    PinnedHost(
        MyAPI,
        pinboard_url="https://pinboard.internal",    # omit -> local mode
        advertise_url="http://10.0.3.7:8000/myapi",  # must uniquely address THIS process
        namespace="/myapi",
    ),
    prefix="/myapi",
)
  • PinnedHost is an APIRouter carrying /{id} that dispatches to the instance.
  • Local mode uses an in-memory directory and speaks the same external API.

Pinboard

A reverse proxy that routes each request to the backend holding the instance, placing instances on registered backends as needed. Requires Redis and a deployment token shared with the backends.

Protocol

All registry calls carry Authorization: Bearer <deployment token>.

  • POST /registry/register at boot — { clientId, secret, namespaces, advertiseUrl, headroom, draining }. Response: { sessionId, expiresInMs }.
  • POST /registry/heartbeat every 3s — { sessionId, headroom, draining, drainDeadline? }. Response: ack { expiresInMs }, or a 409 NACK: reregister (unknown session — abort local instances, register fresh) or superseded.
  • POST /registry/release{ clientId, namespace, id } frees a placement when an instance closes.
  • Pinboard → backend: POST <advertiseUrl>/_pinned/drain { deadline? }, /_pinned/wind_down { id, deadline? }, and /_pinned/resume { id? }, authenticated with x-pinned-proxy-secret.
  • Operator: POST /registry/push delivers a drain/wind_down/resume to a worker; GET /registry/workers and GET /registry/overview introspect; GET /healthz is unauthenticated.
  • clientId and secret are minted once at process start. sessionId is the per-registration lease token: placements belong to the session, and the backend aborts all local instances before serving under a new one.
  • Data-plane requests go over plain HTTP to <advertiseUrl>/<id><rest>. The proxy sets x-pinned-proxy-secret to the worker's secret; the backend rejects a mismatch with 403.
  • Placement is per (namespace, id): first claim wins (Redis SET NX) among live, undrained backends, biased toward headroom. Redis keys and body shapes are documented in pinboard's source (packages/pinned/pinboard).

Lifecycle

  • Activation on first request; deactivation by idle sweep. In-flight requests and open streams block eviction.
  • self.abort() force-ends an instance: cancels in-flight work and tears down.
  • Streams are pull-based: the response generator advances only as the client reads — a slow client backpressures its own stream and nothing else, with no per-connection buffer.
  • Draining workers answer new activations with 503 + Retry-After while existing requests keep serving; freed ids activate on the new deployment.
  • While an instance's close and /registry/release are in flight, requests for that id answer 503 + Retry-After so the proxy re-resolves the placement instead of re-activating locally.

Instance memory is ephemeral. Idle eviction, deploys, lease loss, and crashes all discard it; a re-activation starts from lifespan with nothing. Anything that must survive belongs in the app's own store, written before the response that claims it.

Leases and fencing

  • Each acked heartbeat anchors the lease at send time: self.lease.expires_at = send time + expiresInMs (10s by default, pinboard-supplied) — the moment pinboard may re-place this worker's ids, matching the worker record's Redis TTL.
  • When expires_at - buffer passes without an ack, the worker quits: aborts every local instance and stops registering before the ids can move; the host app keeps running. The buffer (default 5s) is the worker's local safety margin; self.lease.configure(...) changes it and the handlers.
  • A frozen process (VM pause, long GC) that wakes after its lease lapsed fails its next heartbeat (unknown session) and aborts everything before serving again.
  • Guarantee: at most one live instance per id outside a lease-bounded failure window.

Observability

Lifecycle events (activation, eviction/abort, drain, lease health, registration) log on the stdlib pinned.* logger hierarchy with instance/namespace fields; configure it like any library logger. When prometheus_client is importable, pinned.metrics registers pinned_* counters, gauges, and a per-namespace request duration histogram on the default registry — a host app exposing /metrics picks them up automatically. Without the package they are no-ops. No Sentry or OpenTelemetry wiring; that stays in the host app.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pinned-0.1.0.tar.gz (30.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pinned-0.1.0-py3-none-any.whl (20.9 kB view details)

Uploaded Python 3

File details

Details for the file pinned-0.1.0.tar.gz.

File metadata

  • Download URL: pinned-0.1.0.tar.gz
  • Upload date:
  • Size: 30.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pinned-0.1.0.tar.gz
Algorithm Hash digest
SHA256 dc4d022150141c1cad1776e4552910db7c91a87058aed40d599fc93d8066a595
MD5 3116a048d28cd42ec6a3ed383a0e583d
BLAKE2b-256 196ea7ae1bf94505c3588495c645bfc77556758f1d4b9574c6d538999185fda0

See more details on using hashes here.

File details

Details for the file pinned-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pinned-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pinned-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2a8c6daa0b26b85a0ace4fce6b84589dce0c78345fa49915495be20eb210243e
MD5 bc8e8d204df50680f3a5a9a3694e9a28
BLAKE2b-256 cef176d12559e1b42d33788edcdcc38d6cc3f3d7eb1a3a3d30795d3fce539e1d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page