portalocker package¶
Submodules¶
- portalocker.redis module
DEFAULT_THREAD_SLEEP_TIMEDEFAULT_UNAVAILABLE_TIMEOUTPubSubWorkerThreadREDIS_LOCK_PROTOCOL_VERSIONRedisLockRedisLock.DEFAULT_REDIS_KWARGSRedisLock.acquire()RedisLock.channelRedisLock.channel_handler()RedisLock.check_or_kill_lock()RedisLock.client_nameRedisLock.close_connectionRedisLock.connectionRedisLock.flagsRedisLock.get_connection()RedisLock.holder_idRedisLock.legacy_client_nameRedisLock.modeRedisLock.pubsubRedisLock.redis_kwargsRedisLock.release()RedisLock.threadRedisLock.timeoutRedisLock.writer_elected
RedisLockHolderRedisLockMode
- portalocker.constants module
LOCK_EXLOCK_NBLOCK_SHLOCK_UNLockFlagsLockFlags.EXCLUSIVELockFlags.NON_BLOCKINGLockFlags.SHAREDLockFlags.UNBLOCKLockFlags.__abs__()LockFlags.__add__()LockFlags.__and__()LockFlags.__bool__()LockFlags.__ceil__()LockFlags.__contains__()LockFlags.__dir__()LockFlags.__divmod__()LockFlags.__eq__()LockFlags.__float__()LockFlags.__floor__()LockFlags.__floordiv__()LockFlags.__format__()LockFlags.__ge__()LockFlags.__getattribute__()LockFlags.__getitem__()LockFlags.__getnewargs__()LockFlags.__gt__()LockFlags.__hash__()LockFlags.__index__()LockFlags.__init__()LockFlags.__int__()LockFlags.__invert__()LockFlags.__iter__()LockFlags.__le__()LockFlags.__len__()LockFlags.__lshift__()LockFlags.__lt__()LockFlags.__members__LockFlags.__mod__()LockFlags.__module__LockFlags.__mul__()LockFlags.__name__LockFlags.__ne__()LockFlags.__neg__()LockFlags.__new__()LockFlags.__or__()LockFlags.__pos__()LockFlags.__pow__()LockFlags.__qualname__LockFlags.__radd__()LockFlags.__rand__()LockFlags.__rdivmod__()LockFlags.__reduce_ex__()LockFlags.__repr__()LockFlags.__rfloordiv__()LockFlags.__rlshift__()LockFlags.__rmod__()LockFlags.__rmul__()LockFlags.__ror__()LockFlags.__round__()LockFlags.__rpow__()LockFlags.__rrshift__()LockFlags.__rshift__()LockFlags.__rsub__()LockFlags.__rtruediv__()LockFlags.__rxor__()LockFlags.__sizeof__()LockFlags.__str__()LockFlags.__sub__()LockFlags.__truediv__()LockFlags.__trunc__()LockFlags.__xor__()LockFlags._all_bits_LockFlags._boundary_LockFlags._flag_mask_LockFlags._generate_next_value_()LockFlags._get_value()LockFlags._inverted_LockFlags._iter_member_()LockFlags._iter_member_by_def_()LockFlags._iter_member_by_value_()LockFlags._missing_()LockFlags._numeric_repr_()LockFlags._singles_mask_LockFlags.as_integer_ratio()LockFlags.bit_count()LockFlags.bit_length()LockFlags.conjugate()LockFlags.denominatorLockFlags.from_bytes()LockFlags.imagLockFlags.is_integer()LockFlags.numeratorLockFlags.realLockFlags.to_bytes()
- portalocker.exceptions module
- portalocker.portalocker module
- portalocker.utils module
- portalocker.types module
Module contents¶
Cross-platform file locking, with optional Redis-backed distributed locks.
This is the public entry point of the portalocker package. Most of what
you need lives directly on this module:
Lock and RLock open a file and hold an OS-level advisory lock on it for the lifetime of the
withblock; RLock additionally allows the same thread to re-enter the lock.BoundedSemaphore and NamedBoundedSemaphore cap the number of processes that may hold a lock concurrently, using a directory of lock files rather than a single one.
PidFileLock writes the current process ID into the lock file, so a stale lock left behind by a crashed process can be recognised.
TemporaryFileLock is a short-lived variant of Lock intended for small, throwaway critical sections.
RedisLock is a distributed lock built on Redis pubsub, for coordinating processes that do not share a filesystem. It requires the optional
redisdependency; when that package is not installed, RedisLock is still importable asNoneso thatimport portalockernever fails, but constructing it raises at that point rather than at import time. Install it withpip install "portalocker[redis]".lock and unlock are the low-level, platform-specific primitives that the classes above are built on; reach for them only if the context managers do not fit your use case.
Example
>>> import portalocker
>>> with portalocker.Lock('somefile', timeout=1) as fh:
... _ = fh.write('writing some stuff to my cache')
- exception portalocker.AlreadyLocked(*args, holder_pid=None, **kwargs)[source]¶
Bases:
LockExceptionException thrown when the file is already locked by someone else
- class portalocker.BoundedSemaphore(maximum, name='bounded_semaphore', filename_pattern='{name}.{number:02d}.lock', directory='/tmp', timeout=5, check_interval=0.25, fail_when_locked=True)[source]¶
Bases:
LockBase[Lock | None]Bounded semaphore to prevent too many parallel processes from running.
A slot is a lock file: maximum of them are generated from filename_pattern, and acquiring means locking whichever one is still free. Releasing does not delete the files, it only unlocks them.
Prefer NamedBoundedSemaphore, a drop-in replacement for this class. Without an explicit name this class falls back to the shared default name
bounded_semaphore, so two completely unrelated programs on the same machine end up sharing one semaphore; constructing one that way emits a DeprecationWarning. Passing a name here is equivalent and warning-free.>>> semaphore = BoundedSemaphore(2, directory='') >>> str(semaphore.get_filenames()[0]) 'bounded_semaphore.00.lock' >>> str(sorted(semaphore.get_random_filenames())[1]) 'bounded_semaphore.01.lock'
See also
NamedBoundedSemaphore: the same thing with a mandatory or generated name.
- Parameters:
- acquire(timeout=None, check_interval=None, fail_when_locked=None)[source]¶
Take one of the maximum slots.
The slot list is built once with get_filenames, so every attempt sweeps the slots in numerical order and keeps the first one that locks. The sweep repeats until a slot is free or the timeout expires. That order is fixed and identical in every process, so all contenders race for slot
0first.- Parameters:
timeout (float | None) – Overrides timeout for this call. See LockBase.
check_interval (float | None) – Overrides check_interval for this call.
fail_when_locked (bool | None) – Overrides fail_when_locked for this call. Unlike the rest of the retry policy this one is consulted only after the timeout has expired: the semaphore always keeps trying for the full timeout, and this decides whether running out of time raises or returns.
- Returns:
The Lock holding the slot that was taken, which is also stored as the lock attribute. None when no slot became free and fail_when_locked resolves to False.
- Raises:
AlreadyLocked – All slots stayed taken for the whole timeout and fail_when_locked resolves to True.
AssertionError – This instance already holds a slot. Release it before acquiring again.
OSError – Raised straight through from try_lock, for instance FileNotFoundError when directory does not exist. The instance stays usable, so a later call can succeed once the cause is fixed.
- Return type:
Lock | None
- get_filename(number)[source]¶
Build the path of a single slot.
- Parameters:
number (int) – The slot number. Callers normally stay within
range(maximum), but any integer formats fine.- Returns:
directory joined with filename_pattern formatted with the semaphore name and this number.
- Return type:
Example
>>> semaphore = BoundedSemaphore(2, name='example', directory='') >>> str(semaphore.get_filename(1)) 'example.01.lock'
- get_filenames()[source]¶
Return the path of every slot, in order.
Example
>>> semaphore = BoundedSemaphore(2, name='example', directory='') >>> [str(filename) for filename in semaphore.get_filenames()] ['example.00.lock', 'example.01.lock']
- get_random_filenames()[source]¶
Return the path of every slot, in a random order.
A helper for callers that want to spread the contention out themselves: hand the result to try_lock and different processes start their sweep at different slots. acquire does not call this; it sweeps the slots in numerical order, see BoundedSemaphore.
- Returns:
The same paths get_filenames returns, in a random order. The shuffle happens on a fresh list; get_filenames is unaffected.
- Return type:
Example
>>> semaphore = BoundedSemaphore(2, name='example', directory='') >>> names = semaphore.get_random_filenames() >>> sorted(str(filename) for filename in names) ['example.00.lock', 'example.01.lock']
- release()[source]¶
Give the slot back, if this instance holds one.
The lock file itself is left on disk; only the operating system lock is dropped, which is what makes the slot available again. Doing nothing when no slot is held keeps LockBase.__del__ safe.
- Return type:
None
- try_lock(filenames)[source]¶
Try each candidate file once and keep the first one that locks.
A single sweep with no waiting: every candidate is locked with
fail_when_locked=True, so a busy slot is skipped immediately rather than waited on.- Parameters:
filenames (Sequence[str | Path]) – The candidate slot files, tried in the given order.
- Returns:
True when a slot was taken, in which case the lock attribute now holds its Lock. False when every candidate was already taken; the lock attribute is then left alone.
- Raises:
Exception – Anything other than AlreadyLocked coming out of Lock.acquire, such as FileNotFoundError for a missing directory. The lock attribute is reset to None first, so the failure cannot brick the instance for later calls.
- Return type:
- portalocker.LOCK_EX: LockFlags = <LockFlags.EXCLUSIVE: 2>¶
Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time.
- portalocker.LOCK_NB: LockFlags = <LockFlags.NON_BLOCKING: 4>¶
Acquire the lock in a non-blocking fashion.
- portalocker.LOCK_SH: LockFlags = <LockFlags.SHARED: 1>¶
Place a shared lock. More than one process may hold a shared lock for a given file at a given time.
- portalocker.LOCK_UN: LockFlags = <LockFlags.UNBLOCK: 8>¶
Remove an existing lock held by this process.
- class portalocker.Lock(filename, mode='a', timeout=None, check_interval=0.25, fail_when_locked=False, flags=<LockFlags.EXCLUSIVE|NON_BLOCKING: 6>, *, raise_on_release_error=False, **file_open_kwargs)[source]¶
-
Lock manager with built-in timeout.
The class most users want. It opens filename, locks it, retries for as long as the inherited retry policy allows, and hands the open filehandle to the caller. Releasing unlocks and closes that handle; the file itself stays behind, which is what makes the lock usable as a plain data file as well as a mutex.
Example
>>> import portalocker >>> with portalocker.Lock('somefile', 'w', timeout=1) as fh: ... _ = fh.write('locked while the block runs')
Warning
The file is opened before it is locked, so letting open truncate would discard another holder’s data before anybody checks whether the lock is free. That is why a mode containing
wis silently turned intoaand the truncation is deferred to Lock._prepare_fh, which runs only after the lock has been taken.Note
Locking is per open filehandle, not per process. Two Lock instances on the same path in the same process do contend with each other, which is what makes single process examples and tests meaningful.
See also
LockBase: documents the
timeout,check_intervalandfail_when_lockedretry semantics shared by every lock. RLock: the reentrant variant. TemporaryFileLock: removes the lock file on release.- Parameters:
- acquire(timeout=None, check_interval=None, fail_when_locked=None)[source]¶
Open the file, lock it and return the filehandle.
Calling this on a lock that is already held is cheap and safe: the filehandle taken earlier is returned as is, without touching the operating system.
- Parameters:
- Returns:
The open, locked filehandle. It is stored on the instance as well, and stays valid until release.
- Raises:
AlreadyLocked – The first attempt found the file locked and fail_when_locked was set.
LockException – Retrying did not help and timeout expired; the exception is the last one the locking call produced. Also raised, wrapping the original, when something other than contention goes wrong, such as the locking backend refusing the flags.
OSError – Opening the file failed, for instance because the directory does not exist or the mode is not permitted. Only locking failures are translated; failures from open propagate untouched.
- Warns:
UserWarning – A timeout was passed while the lock uses blocking flags, where it has no effect.
- Return type:
Example
>>> import portalocker >>> lock = portalocker.Lock('somefile', timeout=1) >>> fh = lock.acquire() >>> fh is lock.acquire() True >>> lock.release()
- release()[source]¶
Unlock and close the file handle, if this instance holds one.
Doing nothing when no lock is held is deliberate: __del__ calls this on every lock that is collected. Unlocking and closing are both always attempted and the stored handle is always cleared, even when one of the two fails.
- Raises:
Exception – Only when the lock was built with
raise_on_release_error=True. The first failure of the unlock and close pair is raised, chained from the second if both failed. By default such failures are swallowed.- Return type:
None
- exception portalocker.LockException(*args, fh=None, **kwargs)[source]¶
Bases:
BaseLockExceptionException thrown if an error occurred during locking
- class portalocker.LockFlags(*values)[source]¶
Bases:
IntFlagLocking flags enum
- EXCLUSIVE = 2¶
Request an exclusive lock. Only one process may hold EXCLUSIVE (or SHARED) on a given file at the same time; other processes attempting to lock it either block or fail, depending on whether NON_BLOCKING is also set.
- NON_BLOCKING = 4¶
fail with AlreadyLocked immediately if it can’t be acquired right away, instead of blocking until it becomes available.
- Type:
Don’t wait for the lock
- SHARED = 1¶
Request a shared lock. Multiple processes may hold SHARED locks on the same file concurrently, but none may hold EXCLUSIVE while any SHARED lock is held. On Windows this requires the optional
win32extra (pip install "portalocker[win32]"); without it, acquiring a shared lock raises ImportError.
- UNBLOCK = 8¶
Release a lock previously acquired on the same file. Used internally by portalocker.unlock; most callers use a context manager (Lock/RLock) instead of applying this flag directly.
- class portalocker.NamedBoundedSemaphore(maximum, name=None, filename_pattern='{name}.{number:02d}.lock', directory='/tmp', timeout=5, check_interval=0.25, fail_when_locked=True)[source]¶
Bases:
BoundedSemaphoreBounded semaphore to prevent too many parallel processes from running.
The recommended form of BoundedSemaphore: identical behaviour, but the name is either yours or randomly generated, never the shared default that makes unrelated programs collide.
It’s also possible to specify a timeout when acquiring the lock to wait for a resource to become available. This is very similar to threading.BoundedSemaphore but works across multiple processes and across multiple operating systems.
Because this works across multiple processes it’s important to give the semaphore a name. This name is used to create the lock files. If you don’t specify a name, a random name will be generated. This means that you can’t use the same semaphore in multiple processes unless you pass the semaphore object to the other processes.
>>> semaphore = NamedBoundedSemaphore(2, name='test') >>> str(semaphore.get_filenames()[0]) '...test.00.lock'
>>> semaphore = NamedBoundedSemaphore(2) >>> 'bounded_semaphore' in str(semaphore.get_filenames()[0]) True
- class portalocker.PidFileLock(filename='.pid', timeout=5, check_interval=0.25, fail_when_locked=True, flags=<LockFlags.EXCLUSIVE|NON_BLOCKING: 6>)[source]¶
Bases:
TemporaryFileLockA lock that writes the current process PID to the file and can read the PID of the process that currently holds the lock.
When used as a context manager: - Returns None if we successfully acquired the lock - Returns the PID (int) if another process holds the lock
The classic “only one instance of this daemon” lock. Two files are involved: filename holds the readable PID, and a sidecar
<filename>.locknext to it carries the actual operating system lock. The split exists because Windows locking is mandatory, so a lock taken on the PID file itself would stop anyone from reading it.Example
>>> import os >>> import portalocker >>> with portalocker.PidFileLock('somefile.pid') as holder_pid: ... holder_pid is None # None means we are the holder True
See also
PidFileLock.fail_closed: for the common case where a contended lock should abort the block instead of running it.
- Parameters:
- acquire(timeout=None, check_interval=None, fail_when_locked=None)[source]¶
Lock the sidecar file and publish the current PID.
- Parameters:
- Returns:
The filehandle of the sidecar lock file. It exists to satisfy the Lock typing contract; read the PID through PidFileLock.read_pid instead of from this handle.
- Raises:
AlreadyLocked – Somebody else holds the lock. Every plain LockException from the sidecar is normalized to this, so callers have a single exception to catch whether the failure came from fail_when_locked or from an expired timeout.
Exception – Anything else the sidecar Lock raises, such as an OSError from opening it, propagates unchanged. So does a failure to publish the PID, for instance because the filesystem is full; the sidecar lock is rolled back first, so that failure never leaves the lock held, and a rollback error of its own is chained onto the original.
- Return type:
- fail_closed()[source]¶
Return a context that enters only after acquiring this lock.
The fail-closed counterpart of using the lock directly as a context manager: contention aborts before the block runs instead of running it with a PID in hand.
- Returns:
A context manager that binds None and guarantees the block only runs while this process owns the lock.
- Raises:
AlreadyLocked – On entry, when the lock could not be taken. Usually another process holds it, but PidFileLock.acquire also collapses every other plain LockException from the sidecar onto this type. Its
holder_pidattribute carries the competing PID when it could be read.Exception – On entry, anything else PidFileLock.acquire raises, unchanged; the same pass-through class documented on PidFileLock.__enter__.
- Return type:
AbstractContextManager[None]
Example
>>> import portalocker >>> lock = portalocker.PidFileLock('somefile.pid') >>> with lock.fail_closed(): ... print('exclusive work happens here') exclusive work happens here
- read_pid()[source]¶
Read the PID from the lock file, if it exists and is readable.
- Returns:
The PID recorded in the file, or None when the file is missing, empty, unreadable or does not contain a number. Note that a returned PID only says who wrote the file; the process may since have died.
- Return type:
int | None
- release()[source]¶
Release the sidecar lock and remove the PID + sidecar files.
On POSIX both the PID file and the sidecar lock file are unlinked while the sidecar lock is still held, so a competing acquirer cannot grab the sidecar path in the window between unlock and unlink (split-brain). The PID file itself carries no OS lock (the sidecar holds it), but it is removed in the same held window for consistency. On Windows the locked sidecar cannot be unlinked, so it is released first and removed after.
Releasing an object that does not hold the sidecar is a no-op: a stale object (double release, or garbage collection of a failed acquire calling
__del__) must never unlink the PID or sidecar files out from under the current holder.- Return type:
None
- class portalocker.RLock(filename, mode='a', timeout=5, check_interval=0.25, fail_when_locked=False, flags=<LockFlags.EXCLUSIVE|NON_BLOCKING: 6>)[source]¶
Bases:
LockA reentrant lock, functions in a similar way to threading.RLock in that it can be acquired multiple times. When the corresponding number of release() calls are made the lock will finally release the underlying file lock.
Reentrancy is per instance, not per process: it is this object’s own acquire count that is tracked, so a second RLock on the same file still contends with the first one.
Example
>>> import portalocker >>> lock = portalocker.RLock('somefile') >>> fh = lock.acquire() >>> fh is lock.acquire() True >>> lock.release() >>> fh.closed False >>> lock.release() >>> fh.closed True
See also
Lock: the non-reentrant version and the source of every constructor argument.
- Parameters:
- acquire(timeout=None, check_interval=None, fail_when_locked=None)[source]¶
Take the lock, or note that this instance already has it.
The first call locks the file through Lock.acquire. Every later call only bumps the acquire count and hands back the same filehandle, so the arguments are ignored once the lock is held.
- Parameters:
- Returns:
The open, locked filehandle. The same object for every nested acquire.
- Raises:
AlreadyLocked – As Lock.acquire, on the first call only.
LockException – As Lock.acquire, on the first call only.
OSError – As Lock.acquire, on the first call only.
- Return type:
- release()[source]¶
Drop one acquire, unlocking once the count reaches zero.
- Raises:
LockException – Released more often than acquired. That is a bookkeeping error rather than contention, so it is reported rather than ignored, unlike the tolerant Lock.release this eventually delegates to.
- Return type:
None
- class portalocker.RedisLock(channel, connection=None, timeout=None, check_interval=None, fail_when_locked=False, thread_sleep_time=0.1, unavailable_timeout=1, redis_kwargs=None, flags=<LockFlags.EXCLUSIVE: 2>)[source]¶
Bases:
LockBase[RedisLock]An extremely reliable Redis lock based on pubsub.
The lock is held by a subscription kept open by a keep-alive thread.
As opposed to most Redis locking systems based on key/value pairs, this locking method is based on the pubsub system. The big advantage is that if the connection gets killed due to network issues, crashing processes or otherwise, it will still immediately unlock instead of waiting for a lock timeout.
To make sure both sides of the lock know about the connection state it is recommended to set the health_check_interval when creating the redis connection.
- Parameters:
channel (str) – the redis channel to use as locking key.
connection (redis.client.Redis | None) – an optional redis connection if you already have one or if you need to specify the redis connection. A connection given here is never closed by the lock; one created by the lock itself is closed on release.
timeout (float | None) – timeout when trying to acquire a lock
check_interval (float | None) – check interval while waiting
fail_when_locked (bool | None) – after the initial lock failed, return an error or lock the file. This does not wait for the timeout.
thread_sleep_time (float) – sleep time between fetching messages from redis to prevent a busy/wait loop. In the case of lock conflicts this increases the time it takes to resolve the conflict. This should be smaller than the check_interval to be useful.
unavailable_timeout (float) – If the conflicting lock is properly connected this should never exceed twice your redis latency. Note that this will increase the wait time possibly beyond your timeout and is always executed if a conflict arises.
redis_kwargs (dict[str, Any] | None) – The redis connection arguments if no connection is given. The DEFAULT_REDIS_KWARGS are used as default, if you want to override these you need to explicitly specify a value (e.g. health_check_interval=0)
flags (constants.LockFlags) – LockFlags.EXCLUSIVE (the default) or LockFlags.SHARED. Shared holders may coexist, while an exclusive holder waits for all shared holders to release. Other flag combinations are rejected; use fail_when_locked for non-blocking acquisition.
Example
Two readers can hold the same channel at the same time, while a writer would have to wait for both of them:
>>> import fakeredis >>> import portalocker >>> connection = fakeredis.FakeStrictRedis( ... server=fakeredis.FakeServer(), decode_responses=True ... ) >>> reader = portalocker.RedisLock( ... 'shared_channel', ... connection=connection, ... flags=portalocker.LockFlags.SHARED, ... ) >>> other_reader = portalocker.RedisLock( ... 'shared_channel', ... connection=connection, ... flags=portalocker.LockFlags.SHARED, ... ) >>> with reader, other_reader: ... print('both readers are in') both readers are in
- DEFAULT_REDIS_KWARGS: ClassVar[dict[str, Any]] = {'decode_responses': True, 'health_check_interval': 10}¶
- acquire(timeout=None, check_interval=None, fail_when_locked=None)[source]¶
Acquire the lock, retrying until it is free or time runs out.
Every attempt subscribes to the channel if this lock is not subscribed already, then either takes the lock outright or hands a probe result to _resolve_lock_holders:
Count the subscribers. Being the only one means nobody else is on the channel, so the lock is free and is taken immediately; an exclusive lock promotes itself from RedisLockMode.PENDING to RedisLockMode.EXCLUSIVE here. This is the uncontended fast path and costs one round trip.
Otherwise probe the channel with _collect_lock_holders and let _resolve_lock_holders decide.
A failed attempt normally unsubscribes again, so the next iteration subscribes from scratch; an elected writer is the exception and holds on to its subscription between attempts.
fail_when_locked turns the retry loop into a single attempt against a contended channel: rather than polling until the timeout expires against a holder that has demonstrably answered a ping, the first attempt that does not end in ownership raises AlreadyLocked. An elected writer always raises here too, even when no readers remain and the lock could have been taken outright.
If subscribing itself fails, _start_subscription rolls back before re-raising, so the original error propagates with the lock left inactive and the same object can be used again.
- Parameters:
timeout (float | None) – Seconds to keep retrying. Defaults to the instance’s timeout, itself defaulting to utils.DEFAULT_TIMEOUT. Zero still makes exactly one attempt.
check_interval (float | None) – Base seconds to wait between attempts, jittered by _timeout_generator. Defaults to the instance’s check_interval.
fail_when_locked (bool | None) – Raise AlreadyLocked on the first unsuccessful attempt instead of retrying. Defaults to the instance’s fail_when_locked.
- Returns:
This lock, so
with RedisLock(...) as lockbinds the lock itself rather than a file handle.- Raises:
AlreadyLocked – The timeout expired without acquiring the lock, or fail_when_locked was set and the first attempt did not succeed.
AssertionError – This instance is already holding a lock. A RedisLock is not reentrant and holds at most one lock at a time; use a second instance, which gets its own holder_id.
- Return type:
Example
>>> import fakeredis >>> import portalocker >>> connection = fakeredis.FakeStrictRedis( ... server=fakeredis.FakeServer(), decode_responses=True ... ) >>> lock = portalocker.RedisLock( ... 'some_channel', connection=connection ... ) >>> lock.acquire(timeout=1) is lock True >>> lock.release()
- channel_handler(message)[source]¶
Answer a liveness ping with this holder’s record.
Registered as the subscription callback in _start_subscription, so it runs on the PubSubWorkerThread for every message published to channel. Anything that is not a JSON object with a non-empty
response_channelstring is ignored, which keeps unrelated traffic on the channel harmless.The reply is published on the private response channel the prober asked for and carries holder_id, the current mode and REDIS_LOCK_PROTOCOL_VERSION. Answering with the live mode rather than a stored one is what makes the protocol truthful: a writer that is still RedisLockMode.PENDING says so, and a probe therefore learns the state as it was at the moment it asked.
A probing lock is subscribed to its own channel, so it answers its own ping and appears in its own holder list.
- check_or_kill_lock(connection, timeout)[source]¶
Ask whether anyone is alive on the channel, and reap if not.
The public liveness check from before 4.0.0. acquire no longer uses it: it probes with _collect_lock_holders and reaps with _kill_unavailable_locks, which understand individual holders and lock modes. This method predates holder ids and answers only the coarser question “is anybody answering on this channel?”.
The ping is published only after the subscription’s own confirmation frame has been consumed, or the wait for it times out. Redis queues a
subscribeconfirmation the moment a subscription is made; before 4.0.0 the reply poll accepted any message, so that confirmation was read as a reply, a crashed holder was reported as alive and stale locks were never reaped. The poll below now also requirestype == 'message', and draining the confirmation first guarantees that the subscription is active before the ping goes out, so a real reply cannot be published into a subscription that is not listening yet.Note
The reap step matches
CLIENT LISTentries against client_name, which since 4.0.0 carries this instance’s own holder_id. On a live server that name identifies only this lock’s own connection, so another process’s crashed holder is not matched here; reaping across holders is what _kill_unavailable_locks does during acquire.- Parameters:
connection (redis.client.Redis) – The connection to probe and to reap on.
timeout (float) – Seconds to wait for the subscribe confirmation and, separately, for a reply, so a fully silent channel can take up to twice this long.
- Returns:
True as soon as any reply arrives. None when nothing answered in time, after killing the matching pubsub connections. False is never returned.
- Return type:
bool | None
- property client_name: str¶
Name given to this holder’s subscriber connection.
_start_subscription sends
CLIENT SETNAMEover the pubsub connection itself, so the name lands on the connection that actually holds the subscription and shows up against it inCLIENT LIST. _kill_unavailable_locks reads it back the other way around: a listed connection whose name carries a holder_id that did not answer the last ping belongs to a crashed holder, and killing it releases the lock.- Returns:
legacy_client_name with this instance’s holder_id appended.
Example
>>> from portalocker import redis >>> lock = redis.RedisLock('some_channel') >>> lock.client_name == f'some_channel-lock-{lock.holder_id}' True
- connection: redis.client.Redis | None¶
- flags: constants.LockFlags¶
- get_connection()[source]¶
Return the Redis connection, creating one on first use.
A connection handed to the constructor is returned unchanged and is never closed by this class; the caller owns it. A connection created here is owned by the lock, is built from redis_kwargs (with DEFAULT_REDIS_KWARGS filled in), and is closed by release, after which the next call creates a fresh one.
- Returns:
The connection every command from this lock is issued on.
- Return type:
- property legacy_client_name: str¶
Connection name used by portalocker 3.2.0 and older.
Up to and including portalocker 3.2.0 every holder on a channel named its connection
<channel>-lock, with no per-holder suffix, and answered a ping with a bare timestamp string instead of a record. Shared locks need holders to be individually identifiable, so 4.0.0 appended holder_id to the name (see client_name) and replaced the timestamp with a JSON record.The old name is still recognised so that a 4.0.0 holder sharing a channel with an older one stays correct: an old reply is recorded as a single legacy RedisLockMode.EXCLUSIVE holder, which blocks readers and writers alike, and an old connection that stops answering is still reaped by name.
- Returns:
The unsuffixed
<channel>-lockname.
Example
>>> from portalocker import redis >>> redis.RedisLock('some_channel').legacy_client_name 'some_channel-lock'
- mode: RedisLockMode¶
- release()[source]¶
Give up the lock and undo everything acquire set up.
Stops and joins the keep-alive thread, unsubscribes and closes the pubsub connection, and forgets any election this lock had won. A connection the lock created itself is closed and cleared, so the next get_connection builds a fresh one; a connection supplied by the caller is left alone.
Dropping the subscription is not merely cleanup, it is the release: other processes learn the lock is free by no longer seeing this subscriber, with no key to delete and no expiry to wait for.
The same method doubles as the back-off between attempts. _resolve_lock_holders calls it after an unsuccessful probe so that a waiting lock stops being counted as a subscriber, and _start_subscription calls it to roll back a subscribe that failed halfway.
Calling this when nothing was acquired is harmless - it still closes a self-created connection if one exists - which is what makes both that rollback and __del__ safe.
- Return type:
None
- thread: PubSubWorkerThread | None¶
- class portalocker.TemporaryFileLock(filename='.lock', timeout=5, check_interval=0.25, fail_when_locked=True, flags=<LockFlags.EXCLUSIVE|NON_BLOCKING: 6>)[source]¶
Bases:
LockA Lock whose lock file only exists while the lock is held.
Use it when the file is purely a mutex and leaving it behind would be litter. release unlinks the path, and so do the two fallbacks that catch a program which forgets to: LockBase.__del__ when the object is collected, and an atexit handler registered by the constructor when the interpreter shuts down while the lock is still held.
That handler holds a weakref.ref rather than the lock itself, so registering it does not keep the object alive; a lock that is collected earlier simply leaves the handler with nothing to do.
Releasing an instance that does not hold the lock is a no-op. Without that rule a stale object, released twice or finalized after a failed acquire, would unlink the path out from under whoever holds the lock at that moment. Added in 4.0.0 as part of the split-brain fix (#115), together with the inode re-check in acquire.
Example
>>> import os >>> import portalocker >>> lock = portalocker.TemporaryFileLock('somefile.lock') >>> _ = lock.acquire() >>> os.path.isfile('somefile.lock') True >>> lock.release() >>> os.path.isfile('somefile.lock') False >>> lock.release()
See also
PidFileLock: adds the owning PID to the file.
- Parameters:
- acquire(timeout=None, check_interval=None, fail_when_locked=None)[source]¶
Acquire the lock, guarding against split-brain path swaps.
- release()[source]¶
Release the file lock and remove the temporary file.
On POSIX the file is unlinked while the lock is still held, so a competing acquirer cannot grab the freshly created path in the window between unlock and unlink (split-brain). On Windows an open/locked file cannot be unlinked, so there we unlock and close first, then remove with a short retry for AV/scanner share violations.
Releasing an object that holds nothing is a no-op: a stale object (double release, or garbage collection of a failed acquire calling
__del__) must never unlink the path out from under the current holder.- Return type:
None
- portalocker.lock(file, flags)[source]¶
Lock a file. Note that this is an advisory lock on Linux/Unix systems
- portalocker.open_atomic(filename, binary=True)[source]¶
Open a new file for atomic writing without replacing an existing file.
The destination must not exist when entering or publishing the context. If another actor creates it while the context is open, publication raises
FileExistsErrorand leaves that destination untouched.The implementation writes and synchronizes a temporary file in the destination directory, then publishes it with an operation that refuses an existing destination. Windows uses an atomic rename; POSIX uses an atomic hard link, so the POSIX filesystem must support hard links.
https://docs.python.org/3/library/os.html#os.link
>>> filename = 'test_file.txt' >>> if os.path.exists(filename): ... os.remove(filename)
>>> with open_atomic(filename) as fh: ... written = fh.write(b'test') >>> assert os.path.exists(filename) >>> os.remove(filename)
>>> import pathlib >>> path_filename = pathlib.Path('test_file.txt')
>>> with open_atomic(path_filename) as fh: ... written = fh.write(b'test') >>> assert path_filename.exists() >>> path_filename.unlink()