Skip to main content

Process-external local singleton via loopback daemon

Project description

loopback-singleton

loopback-singleton is a lightweight Python package that gives multiple local processes access to a single shared object instance hosted in a background daemon on 127.0.0.1.

It is useful when you want one process-external object (cache, counter, coordinator, adapter, etc.) and you want all local workers to call into that object without standing up a full RPC system.

Current status (v0.2.4)

Current release: 0.2.4.

What works today

  • Local singleton daemon auto-start on first use.
  • Concurrent startup coordination with file locking to reduce duplicate daemons.
  • Authenticated handshake (shared token in runtime dir) between client and daemon.
  • Sequential method execution on the singleton object (single executor queue).
  • Idle TTL auto-shutdown for daemon cleanup.
  • Recovery from stale or corrupted runtime metadata.
  • Cross-platform runtime location strategy (Windows + POSIX fallback behavior).

Installation

pip install loopback-singleton

For local development:

pip install -e .[dev]

Quickstart

Create a module with a factory target (class or callable):

# mypkg/services.py
class Counter:
    def __init__(self):
        self.value = 0

    def inc(self) -> int:
        self.value += 1
        return self.value

    def ping(self) -> str:
        return "pong"

Use local_singleton from any process:

from loopback_singleton import local_singleton

svc = local_singleton(
    name="my-counter",
    factory="mypkg.services:Counter",
    idle_ttl=2.0,
    serializer="pickle",
)

with svc.proxy() as obj:
    print(obj.ping())
    print(obj.inc())

API overview

local_singleton(
    name: str,
    factory: str | callable | type,
    *,
    factory_args: tuple = (),
    factory_kwargs: dict | None = None,
    scope: str = "user",
    idle_ttl: float = 2.0,
    serializer: str = "pickle",
    connect_timeout: float = 0.5,
    start_timeout: float = 3.0,
)
  • name: singleton identity (shared runtime namespace).
  • factory: import string ("module:callable_or_class") or module-level importable callable/class object.
  • factory_args, factory_kwargs: constructor args used when creating singleton instance (factory_kwargs=None behaves as {}).
  • scope: currently only "user" is implemented.
  • idle_ttl: daemon stops after this many seconds with zero active connections.
  • serializer: currently only "pickle" is implemented.
  • connect_timeout, start_timeout: socket/startup tuning.

svc.proxy() returns a dynamic proxy where method calls are forwarded to the daemon.

Additional lifecycle APIs are available on LocalSingletonService:

svc.ensure_started()
info = svc.ping()
svc.shutdown()

Discover running singletons

Use list_local_singletons() to inspect singleton runtime directories without changing daemon state.

from loopback_singleton import list_local_singletons

for item in list_local_singletons():
    print(item.name, item.status, item.host, item.port, item.pid, item.serializer)

Notes:

  • Read-only API: it does not take startup locks, spawn daemons, or clean stale files.
  • Connection-related metadata comes from runtime.bin when available (host, port, pid, serializer, protocol fields).
  • Factory details are included by default (factory_import, factory_args, factory_kwargs), and can be skipped with include_factory=False.
  • Auth secrets are not returned (only the auth file path is exposed in the result object).
  • Directories that contain only auth.bin (without runtime/factory metadata) are ignored by default.
  • Optional probe=True performs best-effort connectivity checks against already-running daemons and enriches reachable / ping_info without auto-starting services.

How it works

  1. Client computes runtime paths for the singleton name.
  2. Client attempts connection using runtime metadata.
  3. If missing/failing, it takes a file lock, cleans stale metadata, and spawns daemon.
  4. Daemon binds ephemeral loopback TCP port, writes runtime metadata, and serves requests.
  5. Each CALL request is executed sequentially against one in-memory object instance.

Lifecycle and robustness scenarios

Scenario 1 — Pass a class directly

from loopback_singleton import local_singleton
from mypkg.services import CounterService

svc = local_singleton(
    "counter",
    factory=CounterService,
    factory_args=(10,),
    factory_kwargs={"step": 2},
)

with svc.proxy() as p:
    assert p.inc() == 12
    assert p.inc() == 14

Scenario 2 — Pass a factory function directly

from loopback_singleton import local_singleton
from mypkg.factories import make_cache

svc = local_singleton(
    "cache",
    factory=make_cache,
    factory_kwargs={"max_items": 1000, "ttl": 60},
)

with svc.proxy() as cache:
    cache.put("k", "v")
    assert cache.get("k") == "v"

Scenario 3 — Warm-up a configured daemon without proxy creation

svc = local_singleton("svc", factory=MyService, factory_args=(...), factory_kwargs={...})
svc.ensure_started()
info = svc.ping()

Scenario 4 — Non-importable factory gets a clear error

svc = local_singleton("x", factory=lambda: object())
# -> TypeError: Factory must be importable (module-level). Pass 'pkg.mod:callable' string instead.

Factory consistency across concurrent clients

For a given singleton name, the first daemon start wins. Subsequent clients must use the same normalized factory + args/kwargs. If they differ, the client fails fast with FactoryMismatchError.

Error model

Main exception classes exported by the package:

  • LoopbackSingletonError (base)
  • DaemonConnectionError
    • ConnectionFailedError
    • HandshakeError
  • ProtocolError (invalid or oversized transport frames/messages)
  • FactoryMismatchError (running daemon factory config differs from requested config)
  • RemoteError (remote traceback payload)

Security notes (important)

This MVP uses pickle for transport serialization. pickle is not safe for untrusted input and can execute arbitrary code.

Use this package only in trusted local environments for now.

Runtime files and cleanup

Runtime files are created under:

  • Windows: %LOCALAPPDATA%/loopback-singleton/<name>/
  • Linux/macOS: $XDG_RUNTIME_DIR/loopback-singleton/<name>/
  • POSIX fallback: ~/.cache/loopback-singleton/<name>/

If startup repeatedly fails due to stale metadata, stop clients and remove the directory for that singleton name.

Known limitations (MVP)

  • Callable/class factories must be importable at module level when passed as objects (lambdas/nested functions are rejected).
  • No identity transparency for proxies (isinstance(proxy, MyType) is not preserved).
  • No magic-method forwarding (__len__, operators, iteration, etc.).
  • Only scope="user" implemented.
  • Only serializer="pickle" implemented (msgpack placeholder exists but not implemented).
  • Transport is loopback TCP only.

Development

Run checks and tests:

ruff check .
pytest -q

Build package:

python -m build

Future work

Planned directions for post-MVP releases:

  • Safer serialization options

    • Implement msgpack serializer path and typed payload envelopes.
    • Add optional schema validation for RPC payloads.
  • Richer proxy semantics

    • Support selected dunder/magic methods.
    • Improve error transport with structured remote exception metadata.
  • Lifecycle and observability

    • Add daemon health/metrics endpoint(s) and lightweight tracing hooks.
    • Expose explicit client APIs for graceful shutdown and restart policies.
  • Scope and deployment flexibility

    • Add additional scope modes beyond per-user.
    • Evaluate optional Unix domain socket transport on POSIX.
  • Robustness and compatibility

    • Protocol version negotiation for rolling upgrades.
    • Expanded stress/regression suite for high-concurrency scenarios.
  • Security hardening

    • Optional mutual-auth improvements and stricter runtime file hardening.
    • Guidance and tooling for locked-down local deployments.

Contributions and issue reports are welcome at:

Project details


Download files

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

Source Distribution

loopback_singleton-0.2.4.tar.gz (33.3 kB view details)

Uploaded Source

Built Distribution

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

loopback_singleton-0.2.4-py3-none-any.whl (20.3 kB view details)

Uploaded Python 3

File details

Details for the file loopback_singleton-0.2.4.tar.gz.

File metadata

  • Download URL: loopback_singleton-0.2.4.tar.gz
  • Upload date:
  • Size: 33.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for loopback_singleton-0.2.4.tar.gz
Algorithm Hash digest
SHA256 89bcb4f46992a1e779668726fd44ac3ed62112920fab8250bc51e15a32a78017
MD5 34c159dd55ad399801ed707dc3b03b3a
BLAKE2b-256 1029804618cd872b06c61fc9e33e029a3486f2df8bf9b60998e09d291bf472e9

See more details on using hashes here.

Provenance

The following attestation bundles were made for loopback_singleton-0.2.4.tar.gz:

Publisher: publish.yml on TovarnovM/loopback_singleton

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file loopback_singleton-0.2.4-py3-none-any.whl.

File metadata

File hashes

Hashes for loopback_singleton-0.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 12c6b784edc1ca584a97ff71073664c519a993d59d7adedd7955940fe8d4b791
MD5 dcb6fed982abf877297b1874e7e1c580
BLAKE2b-256 efe0065ce1a21cd6e2543a02be208dafaa378f33b970cf5389b29c27735cc025

See more details on using hashes here.

Provenance

The following attestation bundles were made for loopback_singleton-0.2.4-py3-none-any.whl:

Publisher: publish.yml on TovarnovM/loopback_singleton

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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