MCPcopy Create free account
hub / github.com/tox-dev/filelock / StrictSoftFileLock

Class StrictSoftFileLock

src/filelock/_strict.py:95–260  ·  view source on GitHub ↗

Portable fail-closed lock based on immutable owner claims.

Source from the content-addressed store, hash-verified

93
94
95class StrictSoftFileLock(BaseFileLock):
96 """Portable fail-closed lock based on immutable owner claims."""
97
98 _preserve_lock_file_supported: bool = True
99 _on_acquired_supported: bool = False
100 #: Age cannot clear a strict claim: expiring one on a clock is the overlap the fail-closed contract exists to rule
101 #: out, so only force_break() removes it.
102 _lifetime_supported: bool = False
103 _lifetime_unsupported_reason: str = "a strict claim is never broken by age, only by force_break()"
104 #: The claim doorway publishes an intent and a held record per owner, so a shared instance must serialize them.
105 _serialize_transitions: bool = True
106 #: Contending processes each publish and rescan several files, so back their retries off across a jittered window
107 #: rather than let them collide on every poll. Seconds; keeps a waiter responsive once it wins.
108 _poll_backoff_cap: float = 0.05
109
110 def _acquire(self) -> None:
111 # Resolve once per acquisition, not per poll: a waiter on a relative path must keep publishing into the
112 # directory it started waiting in even when another thread changes the working directory mid-wait.
113 if (claim_root := self._context.claim_root) is None:
114 claim_root = self._context.claim_root = _canonical(self.lock_file)
115 lock_path = Path(claim_root)
116 coordination_directory = Path(f"{lock_path}{_COORDINATION_SUFFIX}")
117 claim_directory = coordination_directory / _CLAIM_DIRECTORY_NAME
118 ensure_directory_exists(os.fspath(lock_path))
119 _ensure_protocol_directory(self.lock_file, coordination_directory)
120 _ensure_protocol_directory(self.lock_file, claim_directory)
121 if (sentinel_fd := _open_or_create_sentinel(self.lock_file, lock_path, self._open_mode())) is None:
122 return
123 try:
124 sentinel_identity = _file_identity(os.fstat(sentinel_fd))
125 except BaseException as inspection_error: # preserve inspection and descriptor cleanup errors
126 try:
127 os.close(sentinel_fd)
128 except BaseException as close_error: # ruff:ignore[blind-except] # preserve inspection and descriptor cleanup errors
129 _raise_cleanup_errors("strict sentinel inspection cleanup failed", inspection_error, close_error)
130 raise
131 self._mark_descriptor_pending(sentinel_fd, sentinel_identity)
132 try:
133 self._attempt_doorway(claim_directory, sentinel_fd, sentinel_identity)
134 except BaseException:
135 if self._context.pending_lock_file_fd == sentinel_fd:
136 self._discard_doorway(sentinel_fd, sentinel_identity)
137 raise
138
139 def _attempt_doorway(self, claim_directory: Path, sentinel_fd: int, sentinel_identity: tuple[int, int]) -> None:
140 if _read_existing_claims(self.lock_file, claim_directory):
141 self._discard_doorway(sentinel_fd, sentinel_identity)
142 return
143
144 token = secrets.token_hex(_TOKEN_HEX_LENGTH // 2)
145 intent_name = _claim_name("intent", token)
146 intent_path = str(claim_directory / intent_name)
147 try:
148 publication_cleanup_error = _publish_record(intent_path, _claim_record(token), self._open_mode())
149 except _PrivateRecordReclaimedError:
150 self._discard_doorway(sentinel_fd, sentinel_identity)
151 return
152 except (NotImplementedError, OSError) as error:

Calls

no outgoing calls