chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,13 @@
from .plan_read_files_op import plan_read_files_op_with_checkpoint_filter
from .plan_read_op import (
create_checkpoint_filter_op,
plan_read_op_with_checkpoint_filter,
)
from .plan_write_op import plan_write_op_with_checkpoint_writer
__all__ = [
"plan_read_files_op_with_checkpoint_filter",
"create_checkpoint_filter_op",
"plan_read_op_with_checkpoint_filter",
"plan_write_op_with_checkpoint_writer",
]
@@ -0,0 +1,47 @@
"""Checkpoint-aware planner for the V2 ``ReadFiles`` logical operator.
Mirrors :func:`plan_read_op_with_checkpoint_filter` (the V1 ``Read``
variant) so V2 uses the same wrapping ActorPool ``CheckpointFilter``
``MapOperator`` downstream of the read: same
``_CheckpointFilterFn`` / ``_get_checkpoint_map_transformer``, same
memory reservation formula, and ``supports_fusion=False`` so the
filter stays a distinct op.
Registered via ``Planner._get_plan_fns_for_checkpointing`` so it only
runs when ``DataContext.checkpoint_config`` is set *and* the logical
plan is a ``Write`` or ``StreamingSplit`` with a ``ReadFiles`` at the
leaf. V2's plain ``plan_read_files_op`` stays checkpoint-unaware; this
file is the only place V2 reads pick up a checkpoint filter.
"""
from typing import List, Optional
import pyarrow
import pyarrow.fs
from ray.data._internal.execution.interfaces import PhysicalOperator
from ray.data._internal.logical.operators import ReadFiles
from ray.data._internal.planner.checkpoint.plan_read_op import (
create_checkpoint_filter_op,
)
from ray.data._internal.planner.plan_read_files_op import plan_read_files_op
from ray.data.context import DataContext
def plan_read_files_op_with_checkpoint_filter(
data_file_dir: Optional[str],
data_file_filesystem: Optional["pyarrow.fs.FileSystem"],
op: ReadFiles,
physical_children: List[PhysicalOperator],
data_context: DataContext,
) -> PhysicalOperator:
"""Wrap a V2 ``ReadFiles`` physical op with a ``CheckpointFilter``.
Defers all wrapping behavior to V1's :func:`create_checkpoint_filter_op`
so the not-found short-circuit, ``IdColumnCheckpointManager.load_checkpoint``
invocation, actor-pool sizing, and ``supports_fusion=False`` placement stay
in one place across the V1 and V2 read paths.
"""
physical_read_op = plan_read_files_op(op, physical_children, data_context)
return create_checkpoint_filter_op(
physical_read_op, data_context, data_file_dir, data_file_filesystem
)
@@ -0,0 +1,156 @@
from typing import Iterable, List, Optional
import numpy
import pyarrow
import pyarrow.fs as fs
from ray.data._internal.compute import ActorPoolStrategy
from ray.data._internal.execution.interfaces import PhysicalOperator, TaskContext
from ray.data._internal.execution.operators.map_operator import MapOperator
from ray.data._internal.execution.operators.map_transformer import (
BlockMapTransformFn,
MapTransformer,
)
from ray.data._internal.logical.operators import Read
from ray.data._internal.output_buffer import OutputBlockSizeOption
from ray.data._internal.planner.plan_read_op import plan_read_op
from ray.data.block import Block
from ray.data.checkpoint.checkpoint_filter import (
IdColumnCheckpointManager,
NumpyArrayBasedCheckpointFilter,
)
from ray.data.context import DataContext
from ray.data.datasource.path_util import _unwrap_protocol
from ray.types import ObjectRef
CHECKPOINT_MEMORY_SAFETY_FACTOR = 1.5
def create_checkpoint_filter_op(
physical_input_op: PhysicalOperator,
data_context: DataContext,
data_file_dir: Optional[str],
data_file_filesystem: Optional["pyarrow.fs.FileSystem"],
) -> PhysicalOperator:
"""Wrap ``physical_input_op`` with an actor-pool checkpoint filter operator.
Args:
physical_input_op: The upstream physical operator whose output should
be filtered.
data_context: The data context carrying the checkpoint config.
data_file_dir: Directory where data files are written. Used to clean
up orphaned data files from pending (incomplete) checkpoints.
data_file_filesystem: Filesystem for data files. Defaults to the
checkpoint filesystem if not provided.
Returns:
A ``CheckpointFilter`` ``MapOperator`` downstream of
``physical_input_op``, or ``physical_input_op`` itself if there is no
checkpoint data to restore from.
"""
checkpoint_config = data_context.checkpoint_config
# Return the input op directly if:
# 1. the checkpoint directory does not exist.
# 2. no valid files under checkpoint_path (for example, it is an empty directory).
info = checkpoint_config.filesystem.get_file_info(
_unwrap_protocol(checkpoint_config.checkpoint_path)
)
if info.type == fs.FileType.NotFound:
return physical_input_op
checkpoint_manager = IdColumnCheckpointManager(
checkpoint_config=checkpoint_config,
data_context=data_context,
)
# load checkpointed IDs as a numpy ndarray and store it to object store.
checkpointed_ids_ref, checkpointed_ids_size = checkpoint_manager.load_checkpoint(
data_file_dir, data_file_filesystem
)
if not checkpointed_ids_ref:
return physical_input_op
map_transformer = _get_checkpoint_map_transformer(
data_context, checkpointed_ids_ref
)
checkpoint_op = MapOperator.create(
map_transformer=map_transformer,
input_op=physical_input_op,
data_context=data_context,
name="CheckpointFilter",
compute_strategy=ActorPoolStrategy(
min_size=checkpoint_config.checkpoint_actor_pool_min_size,
max_size=checkpoint_config.checkpoint_actor_pool_max_size,
),
ray_remote_args={
"memory": max(
checkpoint_config.checkpoint_actor_memory_bytes,
int(checkpointed_ids_size * CHECKPOINT_MEMORY_SAFETY_FACTOR),
)
},
supports_fusion=False,
)
return checkpoint_op
def plan_read_op_with_checkpoint_filter(
data_file_dir: Optional[str],
data_file_filesystem: Optional["pyarrow.fs.FileSystem"],
op: Read,
physical_children: List[PhysicalOperator],
data_context: DataContext,
) -> PhysicalOperator:
"""Plan the read op to physical operators.
1. If checkpoint is not enabled, or the checkpoint_path is an empty directory,
return the original read physical operator.
2. If the checkpoint is valid, translate the logical read operator into two
physical operators read->map, where the map operator receives blocks from the
read operator and outputs the filtered Blocks.
The implementation of the map operator is `ActorPoolMapOperator`. At runtime
the number of checkpoint-actors is dynamically scaled. The number of actors
is in the range [checkpoint_actor_pool_min_size, checkpoint_actor_pool_max_size].
"""
physical_read_op = plan_read_op(op, physical_children, data_context)
return create_checkpoint_filter_op(
physical_read_op, data_context, data_file_dir, data_file_filesystem
)
class _CheckpointFilterFn:
def __init__(
self,
checkpoint_config,
checkpointed_ids_ref: ObjectRef[numpy.ndarray],
):
self._config = checkpoint_config
self._ref = checkpointed_ids_ref
self._filter = None
def init_checkpoint_filter(self):
"""Called once per actor worker to materialize the filter."""
self._filter = NumpyArrayBasedCheckpointFilter(self._config, self._ref)
def __call__(self, blocks: Iterable[Block], ctx: TaskContext) -> Iterable[Block]:
assert self._filter is not None, "checkpoint filter was not initialized!"
for block in blocks:
filtered_block = self._filter.filter_rows_for_block(block)
if filtered_block.num_rows > 0:
yield filtered_block
def _get_checkpoint_map_transformer(
data_context: DataContext, checkpointed_ids_ref: ObjectRef[numpy.ndarray]
) -> MapTransformer:
fn = _CheckpointFilterFn(data_context.checkpoint_config, checkpointed_ids_ref)
transformer_fn = BlockMapTransformFn(
block_fn=fn,
output_block_size_option=OutputBlockSizeOption.of(
target_max_block_size=data_context.target_max_block_size,
),
)
return MapTransformer(
transform_fns=[transformer_fn],
init_fn=fn.init_checkpoint_filter,
)
@@ -0,0 +1,350 @@
import warnings
from typing import Iterable, List, Tuple
from ray.data._internal.delegating_block_builder import DelegatingBlockBuilder
from ray.data._internal.execution.interfaces import PhysicalOperator
from ray.data._internal.execution.interfaces.task_context import TaskContext
from ray.data._internal.execution.operators.map_transformer import (
BlockMapTransformFn,
)
from ray.data._internal.logical.operators import Write
from ray.data._internal.planner.plan_write_op import (
PENDING_CHECKPOINTS_KWARG_NAME,
WRITE_UUID_KWARG_NAME,
_plan_write_op_internal,
generate_collect_write_stats_fn,
)
from ray.data.block import Block, BlockAccessor
from ray.data.checkpoint.checkpoint_writer import (
CheckpointWriter,
PendingCheckpoint,
)
from ray.data.checkpoint.interfaces import (
InvalidCheckpointingOperators,
)
from ray.data.context import DataContext
from ray.data.datasource.datasink import Datasink
from ray.data.datasource.file_datasink import _FileDatasink
from ray.data.datasource.filename_provider import _split_base_and_ext
def _validate_id_column_exists(id_column: str, block: Block) -> None:
"""Validate that the ID column exists in the block.
Args:
id_column: The name of the ID column to validate.
block: The block to check.
Raises:
ValueError: If the ID column is not present in the block.
"""
block_accessor = BlockAccessor.for_block(block)
if id_column not in block_accessor.column_names():
raise ValueError(
f"ID column {id_column} is "
f"absent in the block to be written. Do not drop or rename "
f"this column."
)
def _combine_blocks(
blocks: Iterable[Block],
) -> Tuple[List[Block], Block]:
"""Combine multiple blocks into a single block.
This is used by checkpoint transforms to match the behavior of _FileDatasink.write(),
which combines all input blocks into one output file.
Args:
blocks: Iterable of blocks to combine.
Returns:
A tuple of (block_list, combined_block) where:
- block_list: The original blocks as a list (for later iteration)
- combined_block: A single block combining all input blocks
"""
block_list = list(blocks)
builder = DelegatingBlockBuilder()
for block in block_list:
builder.add_block(block)
combined_block = builder.build()
return block_list, combined_block
def plan_write_op_with_checkpoint_writer(
op: Write, physical_children: List[PhysicalOperator], data_context: DataContext
) -> PhysicalOperator:
"""Plan a write operation with checkpoint support.
For file-based datasinks (_FileDatasink):
Uses 2-phase commit for atomicity:
1. Pre-write: computes expected paths, write pending checkpoints
2. Write: writes data files
3. Post-write: commits checkpoints (renames pending -> committed)
Writing the pending checkpoint BEFORE the data file is critical: the
pending checkpoint is the source of truth for recovery. If failure occurs
after data write but before commit, recovery finds the pending checkpoint,
deletes the matching data files, and retries cleanly. Writing checkpoints
after data files would be non-atomic — if failure occurs between data
write and checkpoint write, there's no record of which data files are
uncommitted.
For non-file datasinks (SQLDatasink, etc.):
Falls back to post-write checkpointing:
1. Write: Write data to destination
2. Post-write: Write checkpoints
Non-file sinks (SQL, MongoDB, etc.) cannot predict a "file path" - data goes
to database rows or documents. So we fall back to writing data first, then
checkpointing. If failure occurs after data write but before checkpoint
write, the same data may be written again on retry without removing the
old data (at-least-once semantics for non-idempotent operations).
"""
assert data_context.checkpoint_config is not None
datasink = op.datasink_or_legacy_datasource
if not isinstance(datasink, Datasink):
raise InvalidCheckpointingOperators(
f"To enable checkpointing, Write operation must use a "
f"Datasink and not a legacy Datasource, but got: "
f"{type(datasink)}"
)
checkpoint_writer = CheckpointWriter.create(data_context.checkpoint_config)
collect_stats_fn = generate_collect_write_stats_fn()
if isinstance(datasink, _FileDatasink):
# File-based datasink: use 2-phase commit for atomicity
# Pre-write transform: compute expected paths and write pending checkpoints
prepare_checkpoint_fn = _generate_prepare_checkpoint_transform(
data_context, datasink, checkpoint_writer
)
# Post-write transform: commit checkpoints
commit_checkpoint_fn = _generate_commit_checkpoint_transform(checkpoint_writer)
pre_transformations = [
prepare_checkpoint_fn,
]
post_transformations = [
commit_checkpoint_fn,
collect_stats_fn,
]
else:
# Non-file datasink (SQL, Mongo, etc.): fall back to non-atomic checkpoint
# No 2-phase commit - write checkpoint after data write
# This might cause duplicate writes if the write operation is retried.
warnings.warn(
f"Checkpointing with non-file datasink ({type(datasink).__name__}) "
f"uses post-write checkpointing, which provides at-least-once "
f"semantics. If a failure occurs after data is written but before "
f"the checkpoint is saved, duplicate data may be written on retry. "
f"This will be addressed in a future version."
)
write_checkpoint_fn = _generate_non_atomic_write_checkpoint_transform(
data_context, checkpoint_writer
)
post_transformations = [
write_checkpoint_fn,
collect_stats_fn,
]
pre_transformations = []
physical_op = _plan_write_op_internal(
op,
physical_children,
data_context,
post_transformations=post_transformations,
pre_transformations=pre_transformations,
)
return physical_op
def _generate_base_filename(
datasink: _FileDatasink,
ctx: TaskContext,
) -> str:
"""Compute the base filename (without extension) for this task's data files.
This is called BEFORE writing to determine the filename prefix for data files
that will be written by this task. Datasinks may write multiple files (with
partitioning, max_rows_per_file, etc.), all sharing this base filename.
Args:
datasink: The file datasink being used.
ctx: The task context.
Returns:
The base filename without extension (e.g., "write_uuid_000000_000000").
Used both as a checkpoint ID for deterministic naming and as a prefix
for matching data files during recovery.
"""
write_uuid = ctx.kwargs.get(WRITE_UUID_KWARG_NAME)
assert write_uuid is not None, "WRITE_UUID_KWARG_NAME is required"
filename = datasink.filename_provider.get_filename_for_task(
write_uuid, ctx.task_idx
)
# All file datasinks can potentially generate multiple files (e.g., with
# partitioning, max_rows_per_file, etc.). Use prefix matching to handle
# cases like "{filename}-{i}.parquet".
base, _ = _split_base_and_ext(filename)
return base
def _generate_prepare_checkpoint_transform(
data_context: DataContext,
datasink: _FileDatasink,
checkpoint_writer: CheckpointWriter,
) -> BlockMapTransformFn:
"""Generate transform for preparing checkpoints BEFORE data write.
This transform runs BEFORE the data write to enable rollback on failure.
By recording the expected file path in a pending checkpoint first, we can
clean up orphaned data files if the task fails after writing data but
before committing.
Steps:
1. Combines all blocks (matching _FileDatasink behavior)
2. Computes expected data file path prefix from FilenameProvider
3. Writes pending checkpoint with expected path prefix as filename
4. Stores pending checkpoint info in ctx.kwargs for later commit
"""
def prepare_checkpoint(
blocks: Iterable[Block], ctx: TaskContext
) -> Iterable[Block]:
# Combine all blocks to match _FileDatasink.write() behavior
# which combines all input blocks into one output file
block_list, combined_block = _combine_blocks(blocks)
ba = BlockAccessor.for_block(combined_block)
if ba.num_rows() > 0:
# Validate ID column exists
id_column = data_context.checkpoint_config.id_column
_validate_id_column_exists(id_column, combined_block)
# Compute base filename using FilenameProvider
# Note: This only depends on write_uuid and task_idx, NOT block content
# base_filename is the filename without extension, used as checkpoint_id
# for deterministic naming (same on retry, enabling idempotent writes)
base_filename = _generate_base_filename(datasink, ctx)
# Extract ID column data for checkpoint
# Project to the single column first, then convert to Arrow to
# avoid materializing the entire block as an Arrow table.
id_column_data = BlockAccessor.for_block(
ba.select(columns=[id_column])
).to_arrow()[id_column]
# Write pending checkpoint with the base filename as checkpoint_id.
# The checkpoint filename will be {base_filename}.pending.parquet.
# During recovery, the pending checkpoint basename (without
# .pending.parquet) is used as a prefix to match data files.
pending = checkpoint_writer.write_pending_checkpoint(
id_column_data,
checkpoint_id=base_filename,
)
# Store pending checkpoint for commit phase
if pending is not None:
if PENDING_CHECKPOINTS_KWARG_NAME not in ctx.kwargs:
ctx.kwargs[PENDING_CHECKPOINTS_KWARG_NAME] = []
ctx.kwargs[PENDING_CHECKPOINTS_KWARG_NAME].append(pending)
# Return original blocks for the write transform
return iter(block_list)
return BlockMapTransformFn(
prepare_checkpoint,
is_udf=False,
disable_block_shaping=True,
)
def _generate_commit_checkpoint_transform(
checkpoint_writer: CheckpointWriter,
) -> BlockMapTransformFn:
"""Generate transform for committing checkpoints AFTER data write.
This transform runs AFTER the data write succeeds, completing the 2-phase
commit. The commit operation (renaming pending -> committed) is the atomic
point: once committed, the data is considered durably written. If failure
occurs before this point, recovery will find the pending checkpoint and
can safely delete the orphaned data files using the stored path.
Steps:
1. Retrieves pending checkpoints from ctx.kwargs
2. Commits each pending checkpoint (rename pending -> committed)
"""
def commit_checkpoints(
blocks: Iterable[Block], ctx: TaskContext
) -> Iterable[Block]:
# Get pending checkpoints written in pre-write phase
pending_checkpoints: List[PendingCheckpoint] = ctx.kwargs.get(
PENDING_CHECKPOINTS_KWARG_NAME, []
)
# Commit each pending checkpoint
for pending in pending_checkpoints:
checkpoint_writer.commit_checkpoint(pending)
return blocks
return BlockMapTransformFn(
commit_checkpoints,
is_udf=False,
disable_block_shaping=True,
)
def _generate_non_atomic_write_checkpoint_transform(
data_context: DataContext,
checkpoint_writer: CheckpointWriter,
) -> BlockMapTransformFn:
"""Generate transform for writing checkpoints AFTER data write (non-file datasinks).
This is a fallback for non-file datasinks (SQL, Mongo, etc.) that don't
support deletions. Unlike file-based sinks where we can delete orphaned
data files during recovery, these sinks have no way to undo a write once
data has been inserted into rows or documents.
The checkpoint is written directly after the data write completes. This
provides at-least-once semantics: if failure occurs after data write but
before checkpoint write, the same data will be written again on retry
without removing the old data.
For idempotent operations (upserts with unique keys), this is safe. For
non-idempotent operations (inserts), duplicates may result.
TODO: For datasinks that support deletions (e.g., SQL DELETE by ID), we
could store written IDs in pending checkpoints and delete them on recovery,
avoiding duplicates even for non-idempotent operations.
"""
def write_checkpoint(blocks: Iterable[Block], ctx: TaskContext) -> Iterable[Block]:
# Combine all blocks
block_list, combined_block = _combine_blocks(blocks)
ba = BlockAccessor.for_block(combined_block)
if ba.num_rows() > 0:
# Validate ID column exists
id_column = data_context.checkpoint_config.id_column
_validate_id_column_exists(id_column, combined_block)
# Write checkpoint directly (no 2-phase commit)
# No data_file_path since non-file datasinks don't have file paths
checkpoint_writer.write_block_checkpoint(ba)
return iter(block_list)
return BlockMapTransformFn(
write_checkpoint,
is_udf=False,
# NOTE: No need for block-shaping
disable_block_shaping=True,
)