Skip to content

ENH: per-directory files for the directory index-location map - #772

Merged
d-chambers merged 3 commits into
devfrom
index-map-entry-files
Jul 24, 2026
Merged

ENH: per-directory files for the directory index-location map#772
d-chambers merged 3 commits into
devfrom
index-map-entry-files

Conversation

@d-chambers

@d-chambers d-chambers commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Simplified successor to #764: it replaces the shared JSON directory index-location map with one small JSON file per data directory, instead of the SQLite database (with corruption-recovery locks and os.register_at_fork hooks) that #764 proposed.

The index-location map records where a directory's index lives when the data directory itself is read-only, or when a user supplies a custom index path. Its goals are (a) concurrent writers to distinct data directories must not lose each other's entries, and (b) a corrupt entry (see #508) must not break indexing.

What changed

  • One entry file per data directory (<sha256(dir)>.json) under a cache directory, replacing the single shared cache_paths.json. Different directories use different files, so concurrent writers to distinct directories cannot clobber one another.
  • Corruption is per-entry and self-healing. A corrupt/unreadable/malformed entry simply reads as a miss; the next atomic write for that directory self-heals it. No SQLite, no advisory locks, no recovery protocol, no at-fork hooks. Writes swap a sibling temp file into place, so readers never see a half-written entry.
  • The read-only fallback records nothing. It derives a deterministic index filename from a full-sha256 digest of the directory path, so it is rediscoverable without a map entry. Only a user-specified custom index path is recorded.
  • Config option directory_index_map_path becomes directory_index_map_dir (now a directory). The map is a disposable cache; any old shared cache_paths.json is ignored and left in place.

Why not the SQLite approach from #764

For a disposable convenience cache whose only production reads/writes are single-key, per-entry files give the same lost-update and corruption guarantees by construction, with ~10% of the code and none of the process-wide side effects (the os.register_at_fork hooks in #764 serialize every fork in the host process through a library lock). It is also friendlier to NFS home directories, where SQLite locking is unreliable.

Supersedes #764. Part of the free-threading work extracted from #763.

Validation

  • tests/test_io/test_indexer.py, tests/test_io/test_index/, tests/test_utils/test_config.py: pass.
  • Full tests/test_io/: 2053 passed, 63 skipped, 1 xfailed.
  • prek/pre-commit hooks pass on changed files.
  • Independent Codex review: its four findings (read-side deletion racing a concurrent repair; non-UTF-8/malformed payload handling; digest truncation risking a read-only index-file collision; non-UTF-8 path bytes) were all addressed with regressions.

Changelog

  • changed: the external index-location map is stored as one small file per data directory under directory_index_map_dir instead of a shared cache_paths.json, so a corrupt entry only affects its own directory. Any old cache_paths.json is ignored and left in place.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes.
  • documented the new feature with docstrings and/or appropriate doc page.
  • included tests. See testing guidelines.
  • added the "ready_for_review" tag once the PR is ready to be reviewed.

Summary by CodeRabbit

  • New Features

    • Index-location mappings are now stored as separate files in a cache directory, improving isolation between data directories.
    • Unwritable directories receive stable, deterministic index locations.
    • Corrupt mapping entries are safely ignored and can self-heal without affecting other directories.
    • Concurrent updates for different directories no longer overwrite one another.
  • API Changes

    • Replaced the directory_index_map_path setting with directory_index_map_dir.
  • Bug Fixes

    • Corrupt or mismatched mapping data now impacts only the affected directory rather than broader index behavior.

Replace the single shared JSON index-location map with one small JSON
file per data directory under a cache directory. This is the simplified
successor to the SQLite approach in #764.

Different directories use different files, so concurrent writers to
distinct directories cannot lose each other's entries, and a corrupt
entry only affects its own directory and self-heals on the next write
(see #508) -- with no locks, corruption-recovery protocol, or at-fork
hooks. Writes swap a sibling temp file into place, so readers never see
a half-written entry. The read-only-directory fallback derives a
deterministic index name and needs no map entry at all; only a
user-specified custom index path is recorded.

Config option `directory_index_map_path` becomes `directory_index_map_dir`
(a directory). The map is a disposable cache; any old shared
`cache_paths.json` is ignored and left in place.
…ling

- _get_mapped_index_path no longer unlinks corrupt entries on read (that
  could delete a concurrent writer's repair); a miss self-heals on the
  next atomic write instead.
- Treat non-UTF-8 / malformed JSON and non-string/empty index_path
  payloads as misses (catch ValueError; validate shape).
- Use the full sha256 via os.fsencode in _path_digest: avoids a
  read-only index-file-name collision and handles non-UTF-8 path bytes.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b2a9cac-2586-4c67-8cdf-ca1183cb1055

📥 Commits

Reviewing files that changed from the base of the PR and between 286de59 and 34c4bb1.

📒 Files selected for processing (2)
  • dascore/io/index/indexer.py
  • tests/test_io/test_indexer.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_io/test_indexer.py

📝 Walkthrough

Walkthrough

Changes

The index-location map now uses per-directory JSON entries under directory_index_map_dir, with SHA-256-based filenames, validated reads, atomic writes, updated index resolution, tests, fixtures, and changelog documentation.

Index map refactor

Layer / File(s) Summary
Directory mapping configuration and helpers
dascore/config.py, dascore/io/index/indexer.py
Configuration and mapping helpers now support per-directory entries, payload validation, digest-based filenames, and atomic writes.
Index path resolution integration
dascore/io/index/indexer.py, tests/test_io/test_indexer.py
DBDirectoryIndexer records and retrieves mapped paths through the directory store and uses deterministic full-path digests for read-only fallbacks.
Compatibility validation and documented behavior
tests/test_io/test_indexer.py, tests/test_io/test_index/test_index_edge_cases.py, tests/conftest.py, docs/changelog.qmd
Tests cover corrupt and legacy entries, atomic cleanup, and directory separation; fixtures and changelog text use directory_index_map_dir and describe the new persistence behavior.

Possibly related PRs

  • DASDAE/dascore#759: Modifies the same index-map helpers and deterministic fallback naming.
  • DASDAE/dascore#763: Updates the same configuration and indexer mapping wiring.
  • DASDAE/dascore#764: Changes the same directory index-location mapping configuration and resolution logic.

Suggested labels: IO

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving the directory index-location map to per-directory files.
Description check ✅ Passed The description follows the template well, includes the change summary, rationale, validation, and checklist items.
Docstring Coverage ✅ Passed Docstring coverage is 92.59% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch index-map-entry-files

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the IO Work for reading/writing different formats label Jul 24, 2026
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ffd59df) to head (34c4bb1).

Additional details and impacted files
@@            Coverage Diff            @@
##               dev      #772   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          164       164           
  Lines        17357     17356    -1     
=========================================
- Hits         17357     17356    -1     
Flag Coverage Δ
network 48.27% <15.78%> (+0.01%) ⬆️
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread dascore/io/index/indexer.py Outdated
file) and non-UTF-8 filename bytes digest without error.
"""
Return a fresh dict of index locations read from disk.
return hashlib.sha256(os.fsencode(path)).hexdigest()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may also need to support URLs in the future. Please just use fsencode if possible, fallback to string only hash if not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 34c4bb1: _path_digest now tries os.fsencode first (exact local-path bytes) and falls back to a plain string encoding for inputs it rejects — e.g. a remote URL/UPath directory whose __fspath__ isn't available — instead of raising. Added a regression covering the fallback branch.

…nput

Prefer os.fsencode for exact local-path digests, but fall back to a
plain string encoding for inputs it rejects (e.g. remote URL/UPath
directories we may support later) instead of raising.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

IO Work for reading/writing different formats

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant