Skip to content

spool.update() permanently skips files whose mtime predates the last index update #791

Description

@d-chambers

Description

DirectoryIndexer.update() will never index a file whose mtime predates the last index update, even though the file is present on disk and was not indexed before. The omission is permanent: subsequent update() calls keep skipping it.

_get_mtime derives the scan cutoff from the index's stored last_updated_timestamp (indexer.py:263-272), and update() passes it as the timestamp floor to scan_to_df (indexer.py:326), where _iter_filesystem drops any entry with st_mtime < timestamp.

The watermark is on file mtime, but the thing it stands in for is index membership. Those agree only while mtime is monotonic with arrival time, which fails whenever a file is placed in the directory with its original timestamp preserved:

  • cp -p, rsync -t / -a, tar -x, mv from another directory
  • restoring a directory from backup
  • clock skew between the writing host and the indexing host (NFS)
  • a file that failed to scan on an earlier pass (partial or corrupt transfer) and later became readable without its mtime changing

Nothing self-heals. Recovery requires touching the file, deleting the index, or calling update(paths=...) with the file path.

Note the asymmetry: the cutoff is only applied while walking a directory. Passing a file path directly bypasses it, because _iter_filesystem raises NotADirectoryError from os.scandir and yields the path unconditionally (misc.py:220-221). So indexer.update(paths=[missed_file]) recovers the file, while indexer.update() on its parent directory does not.

Example

import os
import tempfile
import time
from pathlib import Path

import dascore as dc

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp)
    patch = dc.get_example_patch()

    # One file indexed normally.
    patch.io.write(path / "a.h5", "dasdae")
    dc.spool(path).update()

    # A second file arrives afterwards but carries an older mtime, as it would
    # from `cp -p`, `rsync -a`, `tar -x`, or `mv` from elsewhere.
    b = path / "b.h5"
    patch.io.write(b, "dasdae")
    old = time.time() - 3600
    os.utime(b, (old, old))

    dc.spool(path).update()
    found = sorted(Path(p).name for p in dc.spool(path).get_contents()["path"])
    print("on disk:", sorted(p.name for p in path.glob("*.h5")))
    print("indexed:", found)

Output:

on disk: ['.dascore_index.h5', 'a.h5', 'b.h5']
indexed: ['a.h5']

b.h5 stays missing from the index no matter how many times update() is called.

Expected behavior

update() should index every readable file in the directory that is not already in the index, regardless of its mtime relative to the last update. The mtime watermark is a sound optimization for re-scanning already-indexed files, but it should not be the sole gate on discovering new ones — that check wants to be against the set of indexed paths.

A cheaper interim fix, if a full listing diff is considered too expensive for large trees: document the limitation prominently on update() and note update(paths=...) as the recovery path.

Versions

  • OS: Ubuntu 24.04 (Linux 6.8.0)
  • DASCore Version: 0.1.18 — the relevant code in indexer.py and hdf5.py is byte-identical on master as of filing, so this is not fixed in 0.1.20
  • Python Version: 3.14.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions