tracker

Cross-process lock manager backed by SQLite.

The entire lock acquisition is a single INSERT ... ON CONFLICT DO UPDATE ... WHERE (UPSERT) statement — one SQL, one rowcount check, truly atomic.

The locks table is created lazily on first OperationalError.

class sayt2.tracker.Tracker(db_path: Path)[source]

SQLite-backed cross-process lock manager.

A single .db file can manage locks for multiple datasets (one row per dataset name). The table and parent directories are created lazily on first use.

Parameters:

db_path – Path to the SQLite database file.

lock_it(name: str, expire: int = 60) str[source]

Atomically acquire a lock for name using a single UPSERT.

  • rowcount == 1 → lock acquired (new row or unlocked/expired row).

  • rowcount == 0 → lock is actively held → TrackerIsLockedError.

Returns:

The lock token (UUID hex).

Raises:

TrackerIsLockedError – if the lock is held by another process.

unlock_it(name: str, lock_token: str) None[source]

Release the lock for name, but only if lock_token matches.

If the token does not match (e.g. the lock expired and was re-acquired by another process), this is a silent no-op — the caller’s lock is already gone.

lock(name: str, expire: int = 60)[source]

Context manager that acquires the lock on entry and guarantees release on exit (even if an exception is raised).

Usage:

tracker = Tracker(db_path)
with tracker.lock("books", expire=60):
    # build index ...