Skip to main content

lnpl-postgres

PostgreSQL binding for lnpl's RepositoryDriver SPI. Implements linkly#121: a real-Postgres RepositoryDriver, verified against lnpl.testing.RepositoryDriverTCK in CI via Testcontainers, and registered under the lnpl.drivers entry-point group as postgres.

Install

pip install lnpl-postgres

This also installs a matching lnpl from PyPI. To try unreleased changes, install from main instead: pip install git+https://github.com/choiyounggi/lnpl-postgres@main.

Usage

Once installed, lnpl discovers this package's driver automatically through the lnpl.drivers entry-point. --backend splits on the first colon into <scheme>:<arg>; for postgres, <arg> is passed verbatim to make_driver as a psycopg conninfo string or URI, e.g.:

--backend postgres:postgresql://user:pass@host:5432/dbname

A keyword/value conninfo string also works, since <arg> is passed through unparsed:

--backend "postgres:host=localhost dbname=app"

Connection pool

lnpl serve builds one driver per request. Each driver borrows a connection from a pool shared by every driver with the same DSN in the process, and returns it when the driver is closed. So the number of server connections one process holds never goes above the pool's maximum, however many requests arrive at once (issue #4). A connection returned in the middle of an uncommitted transaction is rolled back before it is lent again.

Environment variable Default Meaning
LNPL_PG_POOL_MIN_SIZE 2 connections kept open while idle (0 allowed)
LNPL_PG_POOL_MAX_SIZE 20 most connections this process opens to one DSN
LNPL_PG_POOL_TIMEOUT 10 seconds a driver waits for a free connection before failing with DriverError

The values are read once, when the first driver for a DSN is created, and must be whole numbers (MIN_SIZE at most MAX_SIZE; MAX_SIZE and TIMEOUT at least 1), otherwise ValueError. With several server processes (for example gunicorn workers), each has its own pool, so keep processes × LNPL_PG_POOL_MAX_SIZE below the server's max_connections (100 in the stock image). The pool is built lazily in each process, so it works with gunicorn's default fork model; do not use --preload.

Idempotency-Key

lnpl serve honours the Idempotency-Key request header on this backend: the driver implements idempotency_begin, idempotency_finish and idempotency_release. Claims live in the table lnpl_idempotency, keyed by (workflow_id, key) and created by the first driver for a DSN with CREATE TABLE IF NOT EXISTS, so there is no separate migration step.

A claim is committed the moment it is made. The recorded status and body are committed on their own, outside the workflow's transaction, so a run that fails and rolls back still leaves its result for the next request with the same key to replay.

A claim older than lnpl serve's idempotency TTL (LNPL_IDEMPOTENCY_TTL_S, default 24 hours) is deleted the next time the same key is claimed. Rows of keys that are never sent again stay in the table; created_at is epoch milliseconds, so they can be pruned with, for example, DELETE FROM lnpl_idempotency WHERE created_at < <epoch milliseconds>.

If one of these methods is called while the same driver still has a workflow transaction open, it borrows a second connection from the pool, so its write is not undone when that transaction rolls back. In lnpl serve this happens only when an event delivery's workflow raised an unexpected error that left its transaction open and the server then releases that delivery's claim. If the pool has no free connection at that moment (always the case with LNPL_PG_POOL_MAX_SIZE=1, and with any size once every connection is in use), that release fails with DriverError after LNPL_PG_POOL_TIMEOUT seconds and the key stays in progress until the idempotency TTL clears it.

Outbox drain and relay

lnpl outbox drain, lnpl outbox ack and lnpl relay <source> --backend postgres:<dsn> --target <url> work on this backend: the driver implements drain_outbox and ack_outbox, so supports_outbox_drain() is True. A drain lists the rows of lnpl_outbox whose delivered_at is empty, lowest seq first. An ack sets delivered_at (epoch milliseconds) and never deletes the row. Acking a seq that is already delivered does nothing and succeeds; a batch that names a seq with no row is refused as a whole and marks nothing.

A drain locks the rows it returns with FOR UPDATE SKIP LOCKED. The lnpl commands call it outside any transaction, so that lock ends as soon as the drain returns: two lnpl relay processes on one database can both send the same row. That is the at-least-once contract; each event's CloudEvents id is outbox-<seq>, so a receiver can drop repeats. Code that calls begin() before drain_outbox() keeps the drained rows locked until its commit() or rollback(): other drainers skip those rows, and an ack of them from another connection waits until that transaction ends.

Local testing

Requires Docker (used by Testcontainers to spin up a real Postgres instance).

pip install -e .[test]
python -m unittest discover -s tests

On macOS with Docker Desktop, Testcontainers' Ryuk reaper container can fail to start with a socket-mount error (mount source path '.../.docker/run/docker.sock'). If you hit that, disable Ryuk for the run (CI's ubuntu-latest runners don't need this):

TESTCONTAINERS_RYUK_DISABLED=true python -m unittest discover -s tests

Supported lnpl versions

This package depends on lnpl>=0.9,<0.10 from PyPI, so any lnpl 0.9.x release works. 0.9.0 is the first lnpl release with RepositoryDriver.supports_idempotency() and the RepositoryDriverTCK cases that check the idempotency methods, so an older lnpl cannot verify them.

The linkly-sync workflow checks two things on every PR:

  • latest-release fails when PyPI has a newer lnpl that this range excludes (for example 0.10.0). Then widen the upper bound, run the suite, and release.
  • latest-main runs the suite against linkly's main, so a break shows up before linkly releases it.

Releasing

  1. Set version in pyproject.toml and merge that to main.
  2. Tag the merge commit v<version> and push the tag.
  3. release.yml runs the suite, builds the sdist and wheel, and publishes them to PyPI through Trusted Publishing. The PyPI publisher is: project lnpl-postgres, owner choiyounggi, repository lnpl-postgres, workflow release.yml, environment pypi.

PyPI never accepts the same version twice. If the publish job fails for an outside reason (PyPI outage, publisher not registered), re-run only that job with gh run rerun <run-id> --failed. If the cause is in this repo, fix it on main and release the next patch version. Never delete or re-push a tag.

Metadata

Release files for lnpl-postgres 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lnpl-postgres 0.1.0
File Size Uploaded Provenance
lnpl_postgres-0.1.0.tar.gz 26.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for lnpl-postgres 0.1.0
File Interpreter ABI Platform
lnpl_postgres-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.2 kB

Release files / lnpl_postgres-0.1.0.tar.gz

Download URL lnpl_postgres-0.1.0.tar.gz
Size 26.0 kB
Tags Source
SHA-256 checksum
How to use checksums
7666d710fb336c36e0c657bae02454aee923ed6dcfc1b4e96a5624389797a9fa
BLAKE2b-256 checksum
How to use checksums
b30e5f4c55a9c51356aa3ab112ef14d312781c8c6248cfc49bd28a5b43785f9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / lnpl_postgres-0.1.0-py3-none-any.whl

Download URL lnpl_postgres-0.1.0-py3-none-any.whl
Size 15.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
48f44f6deb258c6865a1639bc6137e83505216585791ee8f26f589b971ce4502
BLAKE2b-256 checksum
How to use checksums
c3114ba50ae8140bec72abe363aab9f28b14cdc60a38b9d01164461343a265ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for lnpl-postgres 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for lnpl_postgres-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page