Skip to content

v2.0 wave 1: resource & contract fixes (#201 #202 #203) - #205

Open
tomasvotava wants to merge 5 commits into
v2.0from
v2.0-wave-1
Open

v2.0 wave 1: resource & contract fixes (#201 #202 #203)#205
tomasvotava wants to merge 5 commits into
v2.0from
v2.0-wave-1

Conversation

@tomasvotava

Copy link
Copy Markdown
Owner

First wave of the v2.0 async overhaul (epic #193). Three independent, non-breaking issues bundled into one PR via per-issue worktrees. Targets the long-lived v2.0 branch; master stays on the 1.x patch track.

Summary

  • fix(asyncutils): aiter_any type advertises Callable support it doesn't implement #203 fix(asyncutils): handle plain callables in aiter_any — the signature advertised Callable[[], Iterable[T] | AsyncIterable[T]] but the body only handled isasyncgenfunction / isgeneratorfunction, so a plain lambda: [1, 2, 3] raised TypeError. One-hop callable branch added.
  • fix(catalog): AssetRepartitioner — fd exhaustion, double-enter, BaseException cleanup #202 fix(catalog): bounded writer LRU + reentrancy guard in AssetRepartitioner_get_handler opened one NamedTemporaryFile per partition for the life of the run (5-year hourly = ~43k open fds). __enter__ clobbered _state on re-entry. _get_handler had no BaseException guard. Replaced unbounded state with bounded OrderedDict LRU (default 256, configurable via max_open_writers); evicted writers reopen in a+b and append (fastavro detects the existing container, reuses the sync marker, no header duplication); __enter__ raises RuntimeError on re-entry; tempfile creation now mirrors the _serialize_asset_to_tempfile BaseException/cleanup pattern.
  • perf(catalog): SpooledTemporaryFile for sub-threshold avro serialization #201 perf(catalog): keep small avro payloads in memory_serialize_asset_to_tempfile wrote to a NamedTemporaryFile, closed it, then store_asset_async reopened it by path. New spool_threshold_bytes Catalog kwarg (default 8 MiB) — payloads ≤ threshold serialize to a BytesIO and upload from RAM; larger payloads keep the on-disk path. fastavro only needs seek/tell, which BytesIO provides.

Each issue was implemented in its own worktree on v2.0-wave-1-iss-{201,202,203} and merged into v2.0-wave-1 here.

Verification

  • poetry run pytest -q641 passed, 0 failed on the bundled branch (4 new test_aiter_callable_*, 4 new test_serialize_spool_*, 4 new TestAssetRepartitionerHardening::test_repartitioner_*).
  • poetry run pre-commit run --all-files → ruff, mypy, detect-secrets, all hooks pass.
  • poetry run lint-imports5/5 DDD contracts kept.

Test plan

  • Full pytest suite green on bundled branch
  • Pre-commit (ruff + mypy + detect-secrets) clean
  • Import-linter contracts kept (no new layer violations)
  • CHANGELOG entry added under Unreleased — v2.0 (in progress) / Wave 1
  • Reviewer: confirm the streaming-reopen LRU strategy in fix(catalog): AssetRepartitioner — fd exhaustion, double-enter, BaseException cleanup #202 is acceptable vs the alternatives (concat-on-close, memory-buffer-then-flush) called out in the issue.
  • Reviewer: confirm 8 MiB default for spool_threshold_bytes is reasonable; profile-tune later if needed (per the issue note).

Closes #201
Closes #202
Closes #203

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request enhances the Catalog with a spool_threshold_bytes setting for in-memory Avro serialization and hardens the AssetRepartitioner using an LRU cache for open writers to prevent file descriptor leaks. It also updates aiter_any to handle plain callables. Review feedback identifies a memory efficiency regression where large assets are fully buffered before threshold evaluation and points out a missing try...finally block for the BytesIO buffer during serialization.

Comment thread hyperion/catalog/catalog.py Outdated
Comment thread hyperion/catalog/catalog.py Outdated
tomasvotava and others added 4 commits May 20, 2026 13:24
The aiter_any signature advertises Callable[[], Iterable[T] | AsyncIterable[T]] but the
  body only handled isasyncgenfunction / isgeneratorfunction, so a plain lambda: [1, 2, 3] raised TypeError. Add a one-hop callable branch and corresponding tests.

Closes #203
…nd-trip

_serialize_asset_to_tempfile previously wrote to NamedTemporaryFile then
store_asset_async reopened it by path - a full disk round-trip for even
a 5 KB asset. Add a configurable spool_threshold_bytes (default 8 MiB);
small payloads serialize to BytesIO and upload from the in-memory buffer;
large payloads keep the disk path. fastavro only needs seek/tell, which
BytesIO provides.

Closes #201
…oner

AssetRepartitioner.__enter__ clobbered self._state on re-entry (leaking
any still-open writers) and _get_handler opened one NamedTemporaryFile
per partition for the life of the run -- a 5-year hourly repartition
means ~43k open fds. _get_handler also lacked the BaseException guard
that _serialize_asset_to_tempfile uses.

Add a bounded LRU of open writers (default 256, configurable via
constructor), make __enter__ raise on re-entry instead of silently
clobbering, and mirror the tempfile cleanup pattern around
BaseException. On eviction the writer is flushed/closed but its
NamedTemporaryFile(delete=False) path is retained; a later write to the
same partition reopens it in 'a+b' so fastavro's appendable-container
detection resumes the same avro file (verified via _is_appendable +
round-trip test).

Strategy chosen: streaming-reopen (rather than concat-on-close or
in-memory buffering), since fastavro natively supports re-reading an
existing container header and appending new blocks with the same sync
marker -- no header duplication or partial-container concatenation
needed.

Closes #202

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accumulate per-wave entries under "Unreleased — v2.0 (in progress)";
final v2.0 release PR will consolidate them into the dated section.
@tomasvotava

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces performance and resource management improvements to the data catalog. Key updates include a spooling threshold for Avro serialization to keep small payloads in memory and an LRU cache for open writers in AssetRepartitioner to bound file descriptor usage. Feedback identifies a memory efficiency concern where payloads are fully buffered before thresholding, suggests using standard file modes for consistency, and recommends hardening the partition cleanup logic to ensure all temporary files are removed even if an error occurs during the process.

Comment thread hyperion/catalog/catalog.py Outdated
Comment thread hyperion/catalog/catalog.py Outdated
Comment thread hyperion/catalog/catalog.py Outdated

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces performance optimizations and stability fixes for the catalog and asyncutils modules. Key updates include a memory-spooling mechanism for small Avro payloads and an LRU-based writer management system in the AssetRepartitioner to prevent file descriptor leaks. Feedback from the review highlights a potential OOM risk where large assets are fully buffered in memory before size validation, suggesting the use of SpooledTemporaryFile instead. Additionally, a concern was raised regarding the repartitioning logic, which currently produces concatenated Avro containers with duplicate headers when reopening files, potentially impacting file size and compatibility.

Comment thread hyperion/catalog/catalog.py Outdated
Comment thread hyperion/catalog/catalog.py
…itioner cleanup

Two follow-ups on the wave-1 review:

- Replace the BytesIO+promote-to-disk pattern in _serialize_asset_to_tempfile
  with a single tempfile.SpooledTemporaryFile(max_size=spool_threshold_bytes).
  fastavro now writes into a buffer that stays in memory below the threshold
  and rolls to disk transparently during write — large payloads no longer
  get double-buffered in RAM before being copied to a NamedTemporaryFile.
  Caller path collapses to one branch: always upload from the open handle,
  close on done.

- Isolate per-partition cleanup failures in AssetRepartitioner.__exit__ and
  _finalize_partitions so one bad writer.dump() or unlink() doesn't abandon
  the rest of the open writers and tempfiles. Add a test that monkeypatches
  writer.dump() to raise mid-loop and asserts the surviving partition still
  gets closed and its tempfile unlinked.
@tomasvotava

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces several improvements to the hyperion library. It updates asyncutils to support plain callables in aiter_any, optimizes the Catalog by adding a configurable spool_threshold_bytes to keep small Avro payloads in memory, and enhances AssetRepartitioner with a bounded LRU cache for open writers to prevent file descriptor leaks during large repartitioning tasks. The reviewer identified two potential blocking I/O operations in AssetRepartitioner that should be offloaded to asyncio.to_thread to prevent event loop blocking.

path=entry.path.as_posix(),
partition_date=partition_date.isoformat(),
)
upload_file = entry.path.open("rb")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The entry.path.open("rb") call is a blocking I/O operation. When called from an async function, this can block the event loop, which should be avoided. This operation should be run in a separate thread using asyncio.to_thread.

Suggested change
upload_file = entry.path.open("rb")
upload_file = await asyncio.to_thread(entry.path.open, "rb")

try:
await self.catalog._resolve_storage(asset).put_async(asset.get_path(), file)
finally:
file.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The file.close() call is a blocking I/O operation. When called from an async function, this can block the event loop, which should be avoided. This operation should be run in a separate thread using asyncio.to_thread.

Suggested change
file.close()
await asyncio.to_thread(file.close)

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