ADLS Gen2 (dfs) client pool + capability probe¶
Epic #64, issue #65.
Motivation¶
The platform Storage account has HNS (ADLS Gen2) enabled
(infra/modules/storage.bicep isHnsEnabled: true) and a dfs private
endpoint, but the entire data-plane used BlobServiceClient only — so the HNS
advantages (atomic recursive directory delete, metadata-only rename, true
hierarchical listing) were latent. This change lands the foundation every
later migration issue (#68 read, #69 delete/retention, #71 prepare-db) builds
on, with zero behaviour change by default.
User-facing change¶
None yet. The dfs data-plane is gated OFF behind STORAGE_DFS_ENABLED
(default unset = OFF, charter §12a Rule 4). With the flag off, every code path
keeps using the Blob API exactly as before.
What landed¶
api/services/storage/dfs_client_pool.py— pooledDataLakeServiceClient/FileSystemClientmirroring the provenclient_pool.pyBlobServiceClient pool: thread-local fast path, LRU eviction, idle prune, deadlock-safe credential weakref finalizer, andreset_dfs_service_poolfor test/rotation teardown. Exposesdfs_enabled().api/services/__init__.py— credential rotation now also resets the dfs pool so a rotated MI token invalidates pooled dfs clients (parity with blob).api/tests/conftest.py— per-test reset of the dfs pool for isolation.scripts/dev/probe_capabilities.py— newStorage ADLS Gen2 dfs (data plane)probe. It is gated onSTORAGE_DFS_ENABLED: skips when the feature is OFF (the dfs path is unused, so a missing dfs permission is irrelevant), and is required / fail-closed when the feature is ON. The dfs data-plane reuses the existingStorage Blob Data Contributor/Readerrole — no new RBAC.pyproject.toml+uv.lock— addedazure-storage-file-datalake==12.17.0(verified it does not bump theazure-storage-blob==12.23.1pin; lock diff is +17/-0, one new package only).
API / IaC diff summary¶
- No API route changes.
- No Bicep changes (HNS + dfs PE already provisioned). The probe documents the
required role against
infra/modules/storage.bicep. - New env var
STORAGE_DFS_ENABLED(default OFF).
Validation evidence¶
uv run pytest -q api/tests/test_storage_dfs_client_pool.py→ 27 passed (flag gating, account-name validation, pooling/reuse, LRU eviction, deadlock-safe finalizer defer/drain, idle prune, reset).uv run pytest -q api/tests/test_storage_client_pool.py api/tests/test_storage_data.py→ 42 passed (no regression from the conftest / rotation changes).uv run ruff check api→ clean.- Probe:
probe_dfs_list()raisesSkipProbewith the flag OFF; registered inPROBESas required. - Dependency stability:
git diff --numstat uv.lock=17 0, onlyazure-storage-file-datalakeadded;azure-storage-blobstays12.23.1.
Self-critique (design pass)¶
- Contract: new isolated module, no production consumers yet (D/E/H will use it); rotation + conftest resets wired. ✓
- Liveness: LRU loop bounded by pool size; SDK
retry_total=0+ connection/read timeouts (read_timeout=30 for directory ops, documented). ✓ - Concurrency: mirrors the proven blob-pool locking; deadlock-safe finalizer is unit-tested (defer-when-locked + drain-next-op). ✓
- Security: account-name regex reused; dfs URL re-validates; MI auth; no SAS; no public-access flip. ✓
- Backward-compat: flag default-OFF, additive dep, no pin bump. ✓
- Verdict: no Critical/High; the two Medium notes (dep pin, timeout doc) were closed in the hardening rounds.