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.
Files changed (42) hide show
  1. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/.github/workflows/test.yml +1 -1
  2. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/CHANGELOG.md +19 -0
  3. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/PKG-INFO +12 -1
  4. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/README.md +11 -0
  5. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/metadata_manager.py +12 -0
  6. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/main.py +23 -1
  7. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/pyproject.toml +1 -1
  8. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_metadata_manager.py +20 -0
  9. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/uv.lock +1 -1
  10. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/.env.example +0 -0
  11. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/.gitignore +0 -0
  12. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/__init__.py +0 -0
  13. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/api/__init__.py +0 -0
  14. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/api/errors.py +0 -0
  15. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/api/uc_client.py +0 -0
  16. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/auth/__init__.py +0 -0
  17. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/auth/provider.py +0 -0
  18. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/__init__.py +0 -0
  19. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/auth_manager.py +0 -0
  20. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/data_manager.py +0 -0
  21. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/inode_manager.py +0 -0
  22. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/operations.py +0 -0
  23. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/ram_cache.py +0 -0
  24. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/securable_filter.py +0 -0
  25. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/utils.py +0 -0
  26. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/fs/write_buffer.py +0 -0
  27. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/storage/__init__.py +0 -0
  28. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/fuse4dbricks/storage/persistence.py +0 -0
  29. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_auth_manager.py +0 -0
  30. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_auth_provider.py +0 -0
  31. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_data_manager.py +0 -0
  32. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_e2e_mount.py +0 -0
  33. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_inode_manager.py +0 -0
  34. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_main.py +0 -0
  35. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_operations.py +0 -0
  36. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_persistence.py +0 -0
  37. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_securable_filter.py +0 -0
  38. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_uc_client.py +0 -0
  39. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_uc_client_consistency.py +0 -0
  40. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_uc_client_live.py +0 -0
  41. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_utils.py +0 -0
  42. {fuse4dbricks-0.7.0 → fuse4dbricks-0.7.2}/tests/test_write_buffer.py +0 -0
@@ -54,7 +54,7 @@ jobs:
54
54
  - name: Build a binary wheel and a source tarball
55
55
  run: uv build
56
56
  - name: Store the distribution packages
57
- uses: actions/upload-artifact@v5
57
+ uses: actions/upload-artifact@v7
58
58
  with:
59
59
  name: python-package-distributions
60
60
  path: dist/
@@ -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.0
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()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fuse4dbricks"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "FUSE driver for Databricks Unity Catalog Volumes."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
  # ---------------------------------------------------------------------------
@@ -353,7 +353,7 @@ wheels = [
353
353
 
354
354
  [[package]]
355
355
  name = "fuse4dbricks"
356
- version = "0.6.2"
356
+ version = "0.7.2"
357
357
  source = { editable = "." }
358
358
  dependencies = [
359
359
  { name = "databricks-sdk" },
File without changes
File without changes