Skip to content

example DO NOT MERGE#104

Closed
joshuarobinson wants to merge 1 commit into
mainfrom
example_read_mapped
Closed

example DO NOT MERGE#104
joshuarobinson wants to merge 1 commit into
mainfrom
example_read_mapped

Conversation

@joshuarobinson

@joshuarobinson joshuarobinson commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Example of how we might add a more flexibile mapping system for sourcing byte ranges from different places.

Description

Change description.

{Relates to/Closes} {Task ID}.

Checklist

  • Development PR
    • .release_notes/.unreleased.md
      • Notable changes to the client (i.e. not related to tooling, CI/CD, etc.) from this PR have been added.
  • Release PR
    • CI/CD
      • The default branch pipelines are passing in both GitHub + GitLab (latter for SwiftStack E2E tests).
    • multi-storage-client/pyproject.toml
      • The package version has been bumped.
    • .release_notes/.unreleased.md
      • This file's contents have been moved into a .release_notes/{bumped package version}.md file.

Summary by CodeRabbit

  • New Features

    • Added support for files assembled from multiple byte-range segments, including embedded bytes.
    • Introduced a new storage provider that can present logical files as reversed chunk-based mappings.
    • Expanded package exports so the new mapping and provider types are available directly from the top-level API.
  • Bug Fixes

    • Reading mapped logical paths now returns the combined content correctly instead of using the standard file resolution flow.

Example of how we might add a more flexibile mapping system for sourcing
byte ranges from different places.
@joshuarobinson joshuarobinson requested a review from a team July 9, 2026 16:36
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces range-mapped logical files: new types (FileRangeMapping, InlineBytesMapping, RangeMapping, RangeMappingMetadataProvider) and a MAPPED resolved-path state, client read logic to assemble bytes from ordered mappings, a concrete InvertedFileMetadataProvider implementing reverse-chunked mappings, and corresponding public exports.

Changes

Range mapping feature

Layer / File(s) Summary
RangeMapping types and abstract provider
multi-storage-client/src/multistorageclient/types.py
Adds MAPPED enum state, FileRangeMapping, InlineBytesMapping, RangeMapping union, and abstract RangeMappingMetadataProvider with real_mappings().
Client read support for MAPPED paths
multi-storage-client/src/multistorageclient/client/single.py
Adds _read_mapped helper assembling bytes from ordered mappings and branches read() to use it for MAPPED-state paths served by a RangeMappingMetadataProvider.
InvertedFileMetadataProvider implementation
multi-storage-client/src/multistorageclient/providers/inverted_metadata.py
New provider serving logical files as reverse 1KB chunks, substituting every 5th chunk with inline zero bytes, clipping to requested ranges, and marking unsupported operations/non-writable behavior.
Public exports for new mapping types and provider
multi-storage-client/src/multistorageclient/__init__.py, multi-storage-client/src/multistorageclient/providers/__init__.py
Exposes new mapping types and InvertedFileMetadataProvider as top-level/provider package symbols.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant SingleClient
  participant MetadataProvider
  participant StorageProvider

  Caller->>SingleClient: read(logical_path, byte_range)
  SingleClient->>MetadataProvider: realpath(logical_path)
  MetadataProvider-->>SingleClient: ResolvedPath(state=MAPPED)
  SingleClient->>SingleClient: _read_mapped(logical_path, byte_range)
  SingleClient->>MetadataProvider: real_mappings(logical_path, byte_range)
  MetadataProvider-->>SingleClient: ordered RangeMapping list
  loop for each mapping
    alt FileRangeMapping
      SingleClient->>StorageProvider: read physical byte range
      StorageProvider-->>SingleClient: bytes
    else InlineBytesMapping
      SingleClient->>SingleClient: append inline bytes
    end
  end
  SingleClient-->>Caller: assembled bytes
Loading
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is a placeholder and does not clearly summarize the mapping-system changes in the PR. Rename it to describe the main change, e.g. support for mapped byte-range reads and the new inverted metadata provider.
Description check ⚠️ Warning The description mostly repeats the template and omits a concrete change summary and task reference. Replace placeholders with a real change summary, add the related task ID, and confirm the relevant checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
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 example_read_mapped

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
multi-storage-client/src/multistorageclient/client/single.py (2)

255-271: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Sequential per-segment storage reads in _read_mapped.

Each FileRangeMapping triggers its own blocking get_object call in a simple for loop. For providers that decompose a logical file into many small segments (e.g. the accompanying InvertedFileMetadataProvider with 1 KB chunks), this turns into many sequential round-trips on the read path. Consider fetching FileRangeMapping segments concurrently (the file already uses ThreadPoolExecutor elsewhere, e.g. _register_written_files) while preserving output order for the final b"".join.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@multi-storage-client/src/multistorageclient/client/single.py` around lines
255 - 271, The `_read_mapped` path is issuing blocking `get_object` calls one
segment at a time for each `FileRangeMapping`, which makes many small mapped
files read very slowly. Update `single.py`’s `_read_mapped` to fetch
`FileRangeMapping` segments concurrently, using the same `ThreadPoolExecutor`
pattern already used in `_register_written_files`, while keeping the final byte
assembly in the original mapping order. Preserve `InlineBytesMapping` handling
as-is and make sure `b"".join` still receives parts in sequence.

436-441: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Reuse the first realpath() result here.
RangeMappingMetadataProvider.realpath() is called twice for non-MAPPED reads; cache the first ResolvedPath and reuse its exists/physical_path fields to avoid the extra metadata lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@multi-storage-client/src/multistorageclient/client/single.py` around lines
436 - 441, The read path resolution in single.py is calling
RangeMappingMetadataProvider.realpath() more than once for the same non-MAPPED
request; update the logic around _resolve_read_path and the existing resolved
variable so the first ResolvedPath is reused, and use its exists/physical_path
fields instead of triggering a second metadata lookup.
multi-storage-client/src/multistorageclient/providers/inverted_metadata.py (1)

108-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

list_objects is missing type annotations present on every other overridden method.

path, start_after, end_at, and attribute_filter_expression have no type hints here, unlike get_object_metadata, glob, generate_physical_path, etc. in the same class. As per coding guidelines, "Python code must be enforced by ruff (formatting + linting) and pyright (type checking)" — untyped parameters are likely to be flagged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@multi-storage-client/src/multistorageclient/providers/inverted_metadata.py`
around lines 108 - 109, The InvertedMetadataProvider.list_objects override is
missing the same type annotations used by the other methods in this class, so
add explicit types for path, start_after, end_at, and
attribute_filter_expression while keeping the existing Iterator[ObjectMetadata]
return type. Match the style of get_object_metadata, glob, and
generate_physical_path so the method is fully typed and consistent with the
class’s other overrides.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@multi-storage-client/src/multistorageclient/providers/inverted_metadata.py`:
- Around line 51-136: The public methods in InvertedMetadataProvider are missing
required docstrings. Add Sphinx-style docstrings to realpath and each overridden
MetadataProvider method in the “Unsupported MetadataProvider methods” section,
including list_objects, get_object_metadata, glob, generate_physical_path,
add_file, remove_file, commit_updates, is_writable, allow_overwrites, and
should_use_soft_delete, so their purpose and unsupported behavior are documented
consistently.
- Around line 93-100: The underflow chunk offset calculation in the inverted
metadata mapping is clamping in the wrong order, which can collapse a partial
read to offset 0. Update the logic in the chunk-mapping path that builds
FileRangeMapping entries so the base offset is clamped before adding clip_start,
or otherwise preserve clip_start for the smallest physical chunk. Use the
existing range construction around physical_offset, clip_start, and
Range(offset=..., size=...) to locate and fix the bug.

---

Nitpick comments:
In `@multi-storage-client/src/multistorageclient/client/single.py`:
- Around line 255-271: The `_read_mapped` path is issuing blocking `get_object`
calls one segment at a time for each `FileRangeMapping`, which makes many small
mapped files read very slowly. Update `single.py`’s `_read_mapped` to fetch
`FileRangeMapping` segments concurrently, using the same `ThreadPoolExecutor`
pattern already used in `_register_written_files`, while keeping the final byte
assembly in the original mapping order. Preserve `InlineBytesMapping` handling
as-is and make sure `b"".join` still receives parts in sequence.
- Around line 436-441: The read path resolution in single.py is calling
RangeMappingMetadataProvider.realpath() more than once for the same non-MAPPED
request; update the logic around _resolve_read_path and the existing resolved
variable so the first ResolvedPath is reused, and use its exists/physical_path
fields instead of triggering a second metadata lookup.

In `@multi-storage-client/src/multistorageclient/providers/inverted_metadata.py`:
- Around line 108-109: The InvertedMetadataProvider.list_objects override is
missing the same type annotations used by the other methods in this class, so
add explicit types for path, start_after, end_at, and
attribute_filter_expression while keeping the existing Iterator[ObjectMetadata]
return type. Match the style of get_object_metadata, glob, and
generate_physical_path so the method is fully typed and consistent with the
class’s other overrides.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: a5901396-8d33-41c0-86e2-d676262e8bd7

📥 Commits

Reviewing files that changed from the base of the PR and between 8460f5f and 9c1e32e.

📒 Files selected for processing (5)
  • multi-storage-client/src/multistorageclient/__init__.py
  • multi-storage-client/src/multistorageclient/client/single.py
  • multi-storage-client/src/multistorageclient/providers/__init__.py
  • multi-storage-client/src/multistorageclient/providers/inverted_metadata.py
  • multi-storage-client/src/multistorageclient/types.py

Comment on lines +51 to +136
def realpath(self, logical_path: str) -> ResolvedPath:
if logical_path in self._files:
physical_path, _ = self._files[logical_path]
return ResolvedPath(physical_path=physical_path, state=ResolvedPathState.MAPPED)
return ResolvedPath(physical_path=logical_path, state=ResolvedPathState.UNTRACKED)

def real_mappings(self, logical_path: str, byte_range: Optional[Range] = None) -> list[RangeMapping]:
"""
Return chunks of the physical file in reverse order, with every 5th chunk
replaced by inline zero bytes.

If *byte_range* is given, only return the segments that overlap the requested
logical range, clipped to its boundaries.
"""
physical_path, file_size = self._files[logical_path]
if file_size == 0:
return []

num_chunks = math.ceil(file_size / CHUNK_SIZE)

# Logical byte range we need to cover.
logical_start = byte_range.offset if byte_range else 0
logical_end = (byte_range.offset + byte_range.size) if byte_range else file_size

result: list[RangeMapping] = []
logical_cursor = 0

for i in range(num_chunks):
# Size of this logical chunk (last chunk may be smaller).
chunk_logical_size = min(CHUNK_SIZE, file_size - i * CHUNK_SIZE)
chunk_logical_end = logical_cursor + chunk_logical_size

# Skip chunks entirely outside the requested range.
if chunk_logical_end <= logical_start or logical_cursor >= logical_end:
logical_cursor = chunk_logical_end
continue

# Clip this chunk to the requested range.
clip_start = max(logical_cursor, logical_start) - logical_cursor
clip_end = min(chunk_logical_end, logical_end) - logical_cursor
clipped_size = clip_end - clip_start

if (i + 1) % 5 == 0:
result.append(InlineBytesMapping(data=b"\x00" * clipped_size))
else:
# This chunk corresponds to the (i+1)-th block from the end of the file.
physical_offset = file_size - (i + 1) * CHUNK_SIZE + clip_start
# Clamp to 0 for the very last (smallest) chunk.
physical_offset = max(0, physical_offset)
result.append(FileRangeMapping(physical_path=physical_path, range=Range(offset=physical_offset, size=clipped_size)))

logical_cursor = chunk_logical_end

return result

# -- Unsupported MetadataProvider methods --

def list_objects(self, path, start_after=None, end_at=None, include_directories=False, attribute_filter_expression=None, show_attributes=False) -> Iterator[ObjectMetadata]:
raise NotImplementedError

def get_object_metadata(self, path: str, include_pending: bool = False) -> ObjectMetadata:
raise NotImplementedError

def glob(self, pattern: str, attribute_filter_expression=None) -> list[str]:
raise NotImplementedError

def generate_physical_path(self, logical_path: str, for_overwrite: bool = False) -> ResolvedPath:
raise NotImplementedError

def add_file(self, path: str, metadata: ObjectMetadata) -> None:
raise NotImplementedError

def remove_file(self, path: str) -> None:
raise NotImplementedError

def commit_updates(self) -> None:
raise NotImplementedError

def is_writable(self) -> bool:
return False

def allow_overwrites(self) -> bool:
return False

def should_use_soft_delete(self) -> bool:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Missing docstrings on public methods.

realpath and every "Unsupported MetadataProvider methods" override (list_objects, get_object_metadata, glob, generate_physical_path, add_file, remove_file, commit_updates, is_writable, allow_overwrites, should_use_soft_delete) lack docstrings. As per coding guidelines, "All public Python functions must have docstrings (Sphinx autodoc extracts them)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@multi-storage-client/src/multistorageclient/providers/inverted_metadata.py`
around lines 51 - 136, The public methods in InvertedMetadataProvider are
missing required docstrings. Add Sphinx-style docstrings to realpath and each
overridden MetadataProvider method in the “Unsupported MetadataProvider methods”
section, including list_objects, get_object_metadata, glob,
generate_physical_path, add_file, remove_file, commit_updates, is_writable,
allow_overwrites, and should_use_soft_delete, so their purpose and unsupported
behavior are documented consistently.

Source: Coding guidelines

Comment on lines +93 to +100
if (i + 1) % 5 == 0:
result.append(InlineBytesMapping(data=b"\x00" * clipped_size))
else:
# This chunk corresponds to the (i+1)-th block from the end of the file.
physical_offset = file_size - (i + 1) * CHUNK_SIZE + clip_start
# Clamp to 0 for the very last (smallest) chunk.
physical_offset = max(0, physical_offset)
result.append(FileRangeMapping(physical_path=physical_path, range=Range(offset=physical_offset, size=clipped_size)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Clamp-order bug corrupts partial reads into the underflow chunk.

physical_offset = file_size - (i + 1) * CHUNK_SIZE + clip_start is computed and then clamped with max(0, physical_offset). For the physically-first (logically-last) undersized chunk, the base term is negative; if a byte_range starts partway into that chunk (clip_start > 0), adding clip_start before clamping causes the clamp to discard the offset entirely, collapsing the result to 0 instead of clip_start. This returns the wrong physical bytes for any range read that begins inside that chunk.

🐛 Proposed fix: clamp the base offset before adding `clip_start`
-                physical_offset = file_size - (i + 1) * CHUNK_SIZE + clip_start
-                # Clamp to 0 for the very last (smallest) chunk.
-                physical_offset = max(0, physical_offset)
+                # Clamp the chunk's base offset to 0 for the very last (smallest) chunk,
+                # then add clip_start so partial-range reads land at the correct position.
+                physical_offset = max(0, file_size - (i + 1) * CHUNK_SIZE) + clip_start

Would you like me to draft a regression test covering a byte_range that starts partway into the underflow chunk?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (i + 1) % 5 == 0:
result.append(InlineBytesMapping(data=b"\x00" * clipped_size))
else:
# This chunk corresponds to the (i+1)-th block from the end of the file.
physical_offset = file_size - (i + 1) * CHUNK_SIZE + clip_start
# Clamp to 0 for the very last (smallest) chunk.
physical_offset = max(0, physical_offset)
result.append(FileRangeMapping(physical_path=physical_path, range=Range(offset=physical_offset, size=clipped_size)))
if (i + 1) % 5 == 0:
result.append(InlineBytesMapping(data=b"\x00" * clipped_size))
else:
# This chunk corresponds to the (i+1)-th block from the end of the file.
# Clamp the chunk's base offset to 0 for the very last (smallest) chunk,
# then add clip_start so partial-range reads land at the correct position.
physical_offset = max(0, file_size - (i + 1) * CHUNK_SIZE) + clip_start
result.append(FileRangeMapping(physical_path=physical_path, range=Range(offset=physical_offset, size=clipped_size)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@multi-storage-client/src/multistorageclient/providers/inverted_metadata.py`
around lines 93 - 100, The underflow chunk offset calculation in the inverted
metadata mapping is clamping in the wrong order, which can collapse a partial
read to offset 0. Update the logic in the chunk-mapping path that builds
FileRangeMapping entries so the base offset is clamped before adding clip_start,
or otherwise preserve clip_start for the smallest physical chunk. Use the
existing range construction around physical_offset, clip_start, and
Range(offset=..., size=...) to locate and fix the bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant