Repository navigation
Conversation
|
Superseded by the change map below. |
Change map
|
…cope Snapshot export took (connector, path), gated on object-store sinks, and was the only place a snapshot reached durable storage. - GraphSnapshot.export(SnapshotOutputConfig(connector, path)), in the style of the other output configs; any sink that holds files at the path (FileStoreSink, #112) can take it, and every upload tells a chunked store its size so files up to one part stay plain. - Graph.snapshot(mlops=): with MLOps on, the snapshot is exported under <root>/snapshots/ unless a complete export of the same format and id is there; failures warn. - ConcurrentExportError is a RuntimeError (no retry role). GraphSnapshot.graph_key/data_version are private. The README section moves to docs/mlops.md. Verified: test_snapshot_export.py (30: config form, a filesystem-only chunked sink round trip, stored once in a block, existing export, retry and repair cases), snapshot manifest and training-table suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 2
packages/kumo-core/src/kumo_core/chunked_fs.py: 2
104-114:Minor · Stability & Availability · Quick win Fix the stale-layout race between
_layoutand_forget.
_layoutcomputes a layout without a lock and then stores it in the cache. A writer can finish and call_forget(path)between the compute step and the store step. The reader then stores the old layout after the invalidation.Nothing removes that entry afterward. The
_Readeruses the cached sizes. If the new file is larger, a read returns truncated data without an error. If the sizes differ in another way, the read raisesOSError.The race needs a reader and a writer on the same path in one process.
test_layout_cache_survives_concurrent_opens_and_writesdoes not cover it. In that test, each thread reads and writes only its own file.Use an invalidation counter. Store the layout only if no
_forgetcall ran during the compute step.Proposed fix
def _layout(self, path: str) -> Optional[List[Tuple[str, int]]]: cache = self.__dict__.setdefault('_layouts', {}) layout = cache.get(path) if layout is not None: return layout + epoch = self.__dict__.get('_epoch', 0) size = self._size(path) layout = ([(path, size)] if size is not None else _logical_parts( self._list(path + CHUNKS), path + CHUNKS)) - if layout is not None: + if layout is not None and self.__dict__.get('_epoch', 0) == epoch: cache[path] = layout return layoutIn
_forget, increment the counter before it removes entries:self.__dict__['_epoch'] = self.__dict__.get('_epoch', 0) + 1If you need exact atomicity across threads, guard the counter update and the cache store with one
threading.Lock.Add a test in which one thread writes a path while other threads read the same path.
321-341:Minor · Data Integrity & Integration · Quick win Guard failed
_Writerconstruction from finalization.When
_open_part()fails after_clear(),_Writerremains partially initialized.io.RawIOBasefinalization calls_Writer.close(), whose final branch uploads the empty spool as the plain object._clear()has already removed the old chunked representation.
super().close()directly calls the inherited close method and does not re-enter_Writer.close(). Keep the resource setup inside the guard:Suggested fix
- self._dir = tempfile.mkdtemp(prefix='kumo-chunked-', - dir=spool_root(handler)) - self._spool = open(os.path.join(self._dir, 'spool'), 'wb') - self._filled = 0 - self._parts = 0 - self._written = 0 - self._part: Optional[BinaryIO] = None - self._streams = type(handler)._stream is not ChunkedFileHandler._stream - self._limit = (min(handler.plain_bytes, handler.part_bytes) - if self._streams else handler.part_bytes) - self._plain: Optional[BinaryIO] = None - if self._streams and size is not None: - if size <= handler.part_bytes: - self._plain = handler._stream(path) - else: - self._clear() - self._part = self._open_part() + directory: Optional[str] = None + spool: Optional[BinaryIO] = None + try: + directory = tempfile.mkdtemp(prefix='kumo-chunked-', + dir=spool_root(handler)) + self._dir = directory + spool = open(os.path.join(directory, 'spool'), 'wb') + self._spool = spool + self._filled = 0 + self._parts = 0 + self._written = 0 + self._part: Optional[BinaryIO] = None + self._streams = type(handler)._stream is not ChunkedFileHandler._stream + self._limit = (min(handler.plain_bytes, handler.part_bytes) + if self._streams else handler.part_bytes) + self._plain: Optional[BinaryIO] = None + if self._streams and size is not None: + if size <= handler.part_bytes: + self._plain = handler._stream(path) + else: + self._clear() + self._part = self._open_part() + except BaseException: + if spool is not None: + spool.close() + if directory is not None: + shutil.rmtree(directory, ignore_errors=True) + super().close() + raiseThis prevents the empty-object replacement. It does not restore data already removed by
_clear().

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: vedaant/mlops-holdout-fit-mode
- File: .coderabbit.yaml
- Commit: 893be81
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
aca98211-94a5-45c1-8517-a0cd53672083
Files selected for processing: 10-
packages/kumo-api/kumoapi/plugin.pypackages/kumo-core/src/kumo_core/artifacts.pypackages/kumo-core/src/kumo_core/chunked_fs.pypackages/kumo-core/src/kumo_core/object_store_sink.pypackages/kumo-core/tests/test_chunked_fs.pypackages/kumo-engine/kumo/engine/_scratch.pypackages/kumo-engine/kumo/engine/connector/__init__.pypackages/kumo-engine/kumo/engine/trainer/output_config.pypackages/kumo-engine/test/test_filestore_sink.pypackages/kumo-engine/test/test_prediction_completion.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 6 remain after this review.
-
d2153fe to
db52381
Compare
db52381 to
db74c6c
Compare
Snapshot exports, model bundles and export manifests could only go to object stores: the engine checked for ObjectStoreSinkBackend, and output configs accepted output_paths only for S3, GCS, ADLS and local connectors. A store that is not an object store (a Databricks volume, a Snowflake stage) had no way in, even with a pyarrow filesystem to offer. kumoapi.plugin.FileStoreSink is an optional sink capability: holds_files(connector, path) says from the path alone whether it is a file location rather than a table, and filesystem(connector, path) returns the pyarrow filesystem. ObjectStoreSinkBackend implements it. kumo.engine.connector.is_file_destination/file_store are the engine's one file-vs-table gate; PredictionOutputConfig(output_paths=) and the TrainingTableOutputConfig directory slash use it, so object stores behave as before. kumo_core gains FilesystemSink (the object-store file writers over any filesystem()), upload_bundle/download_bundle (the manifest file last), open_sized (tells a chunked filesystem the size) and PROVENANCE_FILE. The chunked filesystem streams a sized file of up to one part as one plain object, writes frames through a local spool so they stay plain too, spools under the engine scratch directory read at each write, and reads parts into the caller's buffer with one copy. Verified: test_chunked_fs.py (26), test_filestore_sink.py (protocol, output configs, bundles and Parquet over a filesystem-only sink), file connector, completion, preflight, registry and kumo-core export suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
db74c6c to
fc4d709
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 1
packages/kumo-core/src/kumo_core/object_store_sink.py: 1
414-415:Minor · Functional Correctness · Quick win Route file-path training-table exports through
FilesystemSinkwhen the raw sink has nowrite_files.
TrainingTableOutputConfigaccepts a file path for aFileStoreSink.TrainingTable.exportthen readswrite_filesfrom the raw sink and raisesNotImplementedErrorif it is missing. The adapter that provideswrite_filesfor file paths is not selected on this path.Use the adapter only when the raw sink lacks
write_filesand the destination is a file location. This keeps the existing rawwrite_filespath for sinks that already provide it.🐛 Suggested fix
--- a/packages/kumo-engine/kumo/engine/pquery/training_table.py +++ b/packages/kumo-engine/kumo/engine/pquery/training_table.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Optional +from kumo_core.object_store_sink import FilesystemSink from kumo_core.model_plan import TrainingTableGenerationPlan from kumoapi.model_plan import \ TrainingTableGenerationPlan as PublicGenerationPlan from kumoapi.train import TrainingTableSpec +from kumo.engine.connector import is_file_destination from kumo.engine.connector.source_table import SourceTable @@ -54,7 +56,11 @@ connector = output_config.connector - write_files = getattr(connector.sink_backend, "write_files", None) + sink = connector.sink_backend + write_files = getattr(sink, "write_files", None) + if not callable(write_files) and is_file_destination( + connector, output_config.table_name): + write_files = FilesystemSink(sink).write_files if not callable(write_files):

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: vedaant/mlops-filestore-sink
- File: .coderabbit.yaml
- Commit: fc4d709
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
dc4767da-d88b-4cf6-9095-139246272709
Files selected for processing: 6-
packages/kumo-api/kumoapi/json_serde.pypackages/kumo-api/kumoapi/plugin.pypackages/kumo-core/src/kumo_core/artifacts.pypackages/kumo-core/src/kumo_core/object_store_sink.pypackages/kumo-engine/kumo/engine/connector/__init__.pypackages/kumo-engine/kumo/engine/trainer/output_config.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
-
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_sink Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad callback Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…toreRouting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dError Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation The preflight moves from FilesystemSink to ObjectStoreSinkBackend, so S3, GCS and ABFS outputs are refused before scoring too, and a conflict found while writing names the same full location. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd predict Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stack: #53 → #112 → #50 → #60 → #64; #114 on #112; #115 on #64. Merge bottom to top.
Summary
Prediction exports and model bundles can only go to S3, GCS, ADLS or local paths: the engine checks the connector type. Databricks volumes and Snowflake stages hold files too, but one upload is capped (5 GiB on volumes) or has no multipart. This PR lets any sink say which of its paths are files, and gives such stores one shared filesystem:
kumoapi.plugin.FileStoreSink: an optional protocol,holds_files(connector, path)(from the path alone) andfilesystem(connector, path)(apyarrow.fs.FileSystem).kumo_core.chunked_fs.ChunkedFileHandler: a pyarrow filesystem over five store calls (size, list, put, ranged read, remove). A file up topart_bytesis one plain object; a larger one is stored as parts under<name>.chunks/<generation>/and committed by an empty_SUCCESSwritten last.kumo_core.object_store_sink.FilesystemSink: the existing object-store writers over any sink'sfilesystem(). Bundles upload_provenance.jsonlast.kumo_core.object_store_sink.FileStoreRouting: a mixin that sends a sink's file paths toFilesystemSinkand its other paths to its own table methods.ObjectStoreSinkBackend.preflight_write_files: an export into an occupied output directory is refused before scoring, on every object store.No volume or stage sink ships here; #114 and #115 add them.
Usage
Back-compat
ObjectStoreSinkBackendholds every path).output_pathson a sink that does not hold the pathValueError, as before; the message now mentions volume and stage paths.Output directory ... already contains data from another exporterror that was raised after scoring.FileStoreSink_SUCCESSlands.abortis aborted; the previous file stays readable; the error is raised..chunks/file read by Spark or an older enginepart_bytesare plain objects.Testing
kumo-core/tests/test_chunked_fs.py: empty, sub-part, one-part and multi-part files; reads across part boundaries; newest committed generation wins; failed writes keep the old file and abort the stream; listings, deletes, streamed writes, spool location; concurrent rewrites.kumo-engine/test/test_filestore_sink.py: output configs, a bundle round trip, trainer publish and load, and prediction export, the occupied-directory refusal before scoring, and a file-only sink refusing a table through a filesystem-only sink over an in-memory store.Change map: see the
kumo-pr-change-mapcomment.🤖 Generated with Claude Code