fuse4dbricks 0.7.0__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/.github/workflows/test.yml +1 -1
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/CHANGELOG.md +19 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/PKG-INFO +12 -1
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/README.md +11 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/metadata_manager.py +12 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/main.py +23 -1
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/pyproject.toml +1 -1
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_metadata_manager.py +20 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/uv.lock +1 -1
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/.env.example +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/.gitignore +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/__init__.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/api/__init__.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/api/errors.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/api/uc_client.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/auth/__init__.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/auth/provider.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/__init__.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/auth_manager.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/data_manager.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/inode_manager.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/operations.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/ram_cache.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/securable_filter.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/utils.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/write_buffer.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/storage/__init__.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/storage/persistence.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_auth_manager.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_auth_provider.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_data_manager.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_e2e_mount.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_inode_manager.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_main.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_operations.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_persistence.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_securable_filter.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_uc_client.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_uc_client_consistency.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_uc_client_live.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_utils.py +0 -0
- {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_write_buffer.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# 0.7.2 (2026-06-19)
|
|
2
|
+
|
|
3
|
+
- Fix a just-created file or directory being reported as nonexistent for up to
|
|
4
|
+
the negative-cache TTL (~5s) when its path had been stat'd before it existed.
|
|
5
|
+
Probing a not-yet-existing path records a negative (not-found) cache entry,
|
|
6
|
+
and `invalidate()` — called on every mutating op (create/write, `mkdir`,
|
|
7
|
+
`unlink`/`rmdir`, `rename`, truncate) — cleared the attribute and directory
|
|
8
|
+
caches but left that negative entry in place, so a subsequent `stat` kept
|
|
9
|
+
returning `ENOENT` until the entry expired. `invalidate()` now also evicts the
|
|
10
|
+
path's negative-cache entries (for every principal).
|
|
11
|
+
|
|
12
|
+
# 0.7.1 (2026-06-17)
|
|
13
|
+
|
|
14
|
+
- Unmount cleanly on `SIGTERM`/`SIGHUP` (e.g. `systemctl stop`). trio only turns
|
|
15
|
+
`SIGINT` (Ctrl-C) into a clean shutdown; other termination signals killed the
|
|
16
|
+
process before `pyfuse3.close()` ran, leaving the kernel with a stale mount
|
|
17
|
+
that showed up as `d?????????` in `ls`/`df`. All termination signals now run
|
|
18
|
+
the same orderly unwind that unmounts the filesystem.
|
|
19
|
+
|
|
1
20
|
# 0.7.0 (2026-06-17)
|
|
2
21
|
|
|
3
22
|
- Fix files written from a Windows application through WSL being uploaded at
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fuse4dbricks
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: FUSE driver for Databricks Unity Catalog Volumes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zeehio/fuse4dbricks
|
|
6
6
|
Author-email: Sergio Oller <sergioller@gmail.com>
|
|
@@ -261,6 +261,13 @@ user has to provide its own access token.
|
|
|
261
261
|
User=root
|
|
262
262
|
WorkingDirectory=/opt/fuse4dbricks
|
|
263
263
|
ExecStart=/opt/fuse4dbricks/fuse4dbricks_start.sh
|
|
264
|
+
# On stop, fuse4dbricks catches SIGTERM and unmounts itself. Give it time
|
|
265
|
+
# to finish before systemd escalates to SIGKILL (which cannot be caught
|
|
266
|
+
# and would leave a stale mount).
|
|
267
|
+
TimeoutStopSec=30
|
|
268
|
+
# Belt-and-suspenders: if the process was hard-killed and left a stale
|
|
269
|
+
# mount, force-unmount it. '-' ignores failure when nothing is mounted.
|
|
270
|
+
ExecStopPost=-/bin/fusermount3 -uz /Volumes
|
|
264
271
|
Restart=on-failure
|
|
265
272
|
RestartSec=5
|
|
266
273
|
|
|
@@ -268,6 +275,10 @@ user has to provide its own access token.
|
|
|
268
275
|
WantedBy=multi-user.target
|
|
269
276
|
EOF
|
|
270
277
|
|
|
278
|
+
If your mountpoint is not `/Volumes`, update it in `ExecStopPost` to match.
|
|
279
|
+
On systems that ship FUSE 2 rather than FUSE 3, use the absolute path to
|
|
280
|
+
`fusermount` (e.g. `/usr/bin/fusermount`) instead of `fusermount3`.
|
|
281
|
+
|
|
271
282
|
|
|
272
283
|
- Reload the daemon lists
|
|
273
284
|
|
|
@@ -238,6 +238,13 @@ user has to provide its own access token.
|
|
|
238
238
|
User=root
|
|
239
239
|
WorkingDirectory=/opt/fuse4dbricks
|
|
240
240
|
ExecStart=/opt/fuse4dbricks/fuse4dbricks_start.sh
|
|
241
|
+
# On stop, fuse4dbricks catches SIGTERM and unmounts itself. Give it time
|
|
242
|
+
# to finish before systemd escalates to SIGKILL (which cannot be caught
|
|
243
|
+
# and would leave a stale mount).
|
|
244
|
+
TimeoutStopSec=30
|
|
245
|
+
# Belt-and-suspenders: if the process was hard-killed and left a stale
|
|
246
|
+
# mount, force-unmount it. '-' ignores failure when nothing is mounted.
|
|
247
|
+
ExecStopPost=-/bin/fusermount3 -uz /Volumes
|
|
241
248
|
Restart=on-failure
|
|
242
249
|
RestartSec=5
|
|
243
250
|
|
|
@@ -245,6 +252,10 @@ user has to provide its own access token.
|
|
|
245
252
|
WantedBy=multi-user.target
|
|
246
253
|
EOF
|
|
247
254
|
|
|
255
|
+
If your mountpoint is not `/Volumes`, update it in `ExecStopPost` to match.
|
|
256
|
+
On systems that ship FUSE 2 rather than FUSE 3, use the absolute path to
|
|
257
|
+
`fusermount` (e.g. `/usr/bin/fusermount`) instead of `fusermount3`.
|
|
258
|
+
|
|
248
259
|
|
|
249
260
|
- Reload the daemon lists
|
|
250
261
|
|
|
@@ -480,6 +480,18 @@ class MetadataManager:
|
|
|
480
480
|
if attr_cache_key in self._attr_cache:
|
|
481
481
|
del self._attr_cache[attr_cache_key]
|
|
482
482
|
|
|
483
|
+
# A mutating op (create/write/mkdir/rename/truncate/delete) just
|
|
484
|
+
# changed this path's presence, so any cached negative is now stale.
|
|
485
|
+
# Drop it for every principal: invalidate() is synchronous and has
|
|
486
|
+
# no principal in hand, and a just-created path must be immediately
|
|
487
|
+
# visible on this mount rather than hidden until the negative TTL
|
|
488
|
+
# expires. Build the key list first, then delete -- there is no
|
|
489
|
+
# await between, so this stays atomic under trio (mirrors the
|
|
490
|
+
# lock-free contract of the rest of this method).
|
|
491
|
+
stale_negatives = [k for k in self._negative_cache if k[1] == fs_path]
|
|
492
|
+
for key in stale_negatives:
|
|
493
|
+
del self._negative_cache[key]
|
|
494
|
+
|
|
483
495
|
parent_path = "/".join(fs_path.rstrip("/").split("/")[:-1]) or "/"
|
|
484
496
|
if parent_path in self._dir_cache:
|
|
485
497
|
del self._dir_cache[parent_path]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import argparse
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import signal
|
|
5
6
|
import traceback
|
|
6
7
|
import sys
|
|
7
8
|
|
|
@@ -145,6 +146,26 @@ def setup_logging(debug_mode):
|
|
|
145
146
|
logging.getLogger("pyfuse3").setLevel(logging.INFO)
|
|
146
147
|
|
|
147
148
|
|
|
149
|
+
async def _shutdown_on_signal(cancel_scope: trio.CancelScope):
|
|
150
|
+
"""Translate termination signals into a clean trio shutdown.
|
|
151
|
+
|
|
152
|
+
Without this, trio only converts SIGINT (Ctrl-C) into a KeyboardInterrupt;
|
|
153
|
+
SIGTERM (e.g. ``systemctl stop``) and SIGHUP kill the process outright, so
|
|
154
|
+
``start_fuse``'s ``finally`` never runs and ``pyfuse3.close()`` never gets
|
|
155
|
+
a chance to unmount. The kernel is then left with a stale mount that shows
|
|
156
|
+
up as ``d?????????`` in ``ls``/``df``. Cancelling the nursery here makes
|
|
157
|
+
every signal follow the same orderly unwind that unmounts the filesystem.
|
|
158
|
+
"""
|
|
159
|
+
with trio.open_signal_receiver(signal.SIGTERM, signal.SIGINT, signal.SIGHUP) as signals:
|
|
160
|
+
async for signum in signals:
|
|
161
|
+
logging.info(
|
|
162
|
+
"Received %s, unmounting and shutting down...",
|
|
163
|
+
signal.Signals(signum).name,
|
|
164
|
+
)
|
|
165
|
+
cancel_scope.cancel()
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
|
|
148
169
|
async def start_fuse(ops, mountpoint, allow_other, debug_mode, stopped_evt: trio.Event | None = None):
|
|
149
170
|
mount_options = ["fsname=fuse4dbricks", "noatime"]
|
|
150
171
|
if allow_other:
|
|
@@ -165,7 +186,7 @@ async def start_fuse(ops, mountpoint, allow_other, debug_mode, stopped_evt: trio
|
|
|
165
186
|
pass
|
|
166
187
|
finally:
|
|
167
188
|
try:
|
|
168
|
-
pyfuse3.close()
|
|
189
|
+
pyfuse3.close(unmount=True)
|
|
169
190
|
finally:
|
|
170
191
|
if stopped_evt is not None:
|
|
171
192
|
stopped_evt.set()
|
|
@@ -251,6 +272,7 @@ async def async_main():
|
|
|
251
272
|
async with trio.open_nursery() as nursery:
|
|
252
273
|
persistence.run_services(nursery)
|
|
253
274
|
data_manager.run_services(nursery)
|
|
275
|
+
nursery.start_soon(_shutdown_on_signal, nursery.cancel_scope)
|
|
254
276
|
nursery.start_soon(start_fuse, operations, mountpoint, args.allow_other, args.debug, stopped_evt)
|
|
255
277
|
|
|
256
278
|
await stopped_evt.wait()
|
|
@@ -453,6 +453,26 @@ def test_invalidate_nonexistent_path_does_not_raise(manager):
|
|
|
453
453
|
manager.invalidate("/nonexistent/path/file.txt", is_dir=False) # Must not raise
|
|
454
454
|
|
|
455
455
|
|
|
456
|
+
def test_invalidate_clears_negative_cache_for_all_principals(manager, ctx):
|
|
457
|
+
"""A mutating op (create/write/mkdir/...) routes through invalidate(); it
|
|
458
|
+
must drop the path's negative entry for EVERY principal so a just-created
|
|
459
|
+
file is immediately visible instead of hidden until the negative TTL
|
|
460
|
+
expires. This is the negative-cache poisoning bug: stat a path before it
|
|
461
|
+
exists, create it, then stat again -> stale ENOENT for ~ttl_negative."""
|
|
462
|
+
future = time.time() + 100
|
|
463
|
+
fs_path = "/cat/sch/vol/file.txt"
|
|
464
|
+
manager._negative_cache[("alice@example.com", fs_path)] = future
|
|
465
|
+
manager._negative_cache[("bob@example.com", fs_path)] = future
|
|
466
|
+
# A negative for a *different* path must survive.
|
|
467
|
+
manager._negative_cache[("alice@example.com", "/cat/sch/vol/other.txt")] = future
|
|
468
|
+
|
|
469
|
+
manager.invalidate(fs_path, is_dir=False)
|
|
470
|
+
|
|
471
|
+
assert ("alice@example.com", fs_path) not in manager._negative_cache
|
|
472
|
+
assert ("bob@example.com", fs_path) not in manager._negative_cache
|
|
473
|
+
assert ("alice@example.com", "/cat/sch/vol/other.txt") in manager._negative_cache
|
|
474
|
+
|
|
475
|
+
|
|
456
476
|
# ---------------------------------------------------------------------------
|
|
457
477
|
# reauthorize (token change -> refresh the uid -> principal mapping)
|
|
458
478
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|