blob: 9a5ca4511e4cda4d2499ce148515ac2985bf1491 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Ray coordination uses public hooks without selecting either file backend."""
from pathlib import Path
import pickle
import subprocess
import sys
from types import SimpleNamespace
from unittest.mock import Mock
import pyarrow as pa
from pypaimon import Schema
from pypaimon.write.ray_datasink import PaimonDatasink, PaimonWriteResult
def test_ray_datasink_configuration_and_pickle_do_not_load_execution():
source = r'''
import importlib.abc
import pickle
import sys
from types import SimpleNamespace
class BlockExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(('pypaimon.execution.python', 'pypaimon.execution.native',
'pypaimon.execution.datasink', 'pypaimon_rust')):
raise AssertionError('Ray configuration loaded execution: ' + fullname)
sys.meta_path.insert(0, BlockExecution())
from pypaimon.write.ray_datasink import PaimonDatasink, PaimonWriteResult
class Identifier:
def get_full_name(self):
return 'default.data'
table = SimpleNamespace(identifier=Identifier())
sink = PaimonDatasink(table, static_partition={'p': 'value'})
assert sink._is_overwrite()
restored = pickle.loads(pickle.dumps(sink))
assert type(restored) is PaimonDatasink
assert PaimonDatasink.__module__ == 'pypaimon.write.ray_datasink'
assert restored.__getstate__().keys() == sink.__getstate__().keys()
assert restored.static_partition == {'p': 'value'}
result = PaimonWriteResult(7, 11)
assert pickle.loads(pickle.dumps(result)) == result
assert type(result).__module__ == 'pypaimon.write.ray_datasink'
'''
result = subprocess.run(
[sys.executable, '-c', source],
cwd=str(Path(__file__).resolve().parents[3]),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
def test_ray_worker_write_retains_public_hook_and_serializable_row_count():
class HookedSink(PaimonDatasink):
def _is_overwrite(self):
self.overwrite_checks += 1
return True
table = Mock()
table.identifier.get_full_name.return_value = 'default.data'
table.table_schema = Schema.from_pyarrow_schema(pa.schema([('id', pa.int64())]))
builder = table.new_batch_write_builder.return_value
builder.overwrite.return_value = builder
writer = builder.new_write.return_value
writer.prepare_commit.return_value = []
sink = HookedSink(table, static_partition={'p': 'value'})
sink.overwrite_checks = 0
messages = sink.write([pa.table({'id': [1, 2, 3]})], None)
assert sink.overwrite_checks == 1
builder.overwrite.assert_called_once_with({'p': 'value'})
writer.write_arrow.assert_called_once()
assert writer.write_arrow.call_args[0][0].to_pydict() == {'id': [1, 2, 3]}
writer.prepare_commit.assert_called_once_with()
writer.close.assert_called_once_with()
writer.abort.assert_not_called()
restored = pickle.loads(pickle.dumps(messages))
assert restored == []
assert restored.row_count == 3
assert sink._writer_builder is None
def test_ray_commit_retains_public_hooks_and_reports_committed_snapshot():
class HookedSink(PaimonDatasink):
def on_write_start(self, schema=None):
self.starts += 1
return super().on_write_start(schema)
@staticmethod
def _extract_row_count(write_result, write_returns):
return 23
table = Mock()
table.identifier.get_full_name.return_value = 'default.data'
builder = table.new_batch_write_builder.return_value
commit = builder.new_commit.return_value
callbacks = []
commit.add_commit_callback.side_effect = callbacks.append
def publish(messages):
for callback in callbacks:
callback.call(SimpleNamespace(snapshot=SimpleNamespace(id=7)))
commit.commit.side_effect = publish
message = Mock()
message.is_empty.return_value = False
sink = HookedSink(table)
sink.starts = 0
sink.on_write_complete([[message]])
assert sink.starts == 1
assert sink.commit_result == PaimonWriteResult(23, 7)
commit.commit.assert_called_once_with([message])
commit.close.assert_called_once_with()
table.snapshot_manager.assert_not_called()
def test_ray_coordination_can_run_without_importing_either_backend():
source = r'''
import importlib.abc
import sys
from types import SimpleNamespace
from unittest.mock import Mock
class BlockBackends(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(('pypaimon.execution.python', 'pypaimon.execution.native', 'pypaimon_rust')):
raise AssertionError('Ray coordination selected a backend directly: ' + fullname)
sys.meta_path.insert(0, BlockBackends())
import pyarrow as pa
from pypaimon import Schema
from pypaimon.write.ray_datasink import PaimonDatasink, PaimonWriteResult
table = Mock()
table.identifier.get_full_name.return_value = 'default.data'
table.table_schema = Schema.from_pyarrow_schema(pa.schema([('id', pa.int64())]))
builder = table.new_batch_write_builder.return_value
writer = builder.new_write.return_value
message = Mock()
message.is_empty.return_value = False
writer.prepare_commit.return_value = [message]
commit = builder.new_commit.return_value
callbacks = []
commit.add_commit_callback.side_effect = callbacks.append
def publish(messages):
for callback in callbacks:
callback.call(SimpleNamespace(snapshot=SimpleNamespace(id=19)))
commit.commit.side_effect = publish
sink = PaimonDatasink(table)
messages = sink.write([pa.table({'id': [1, 2]})], None)
assert messages == [message]
sink.on_write_complete([messages])
assert sink.commit_result == PaimonWriteResult(2, 19)
sink.on_write_failed(RuntimeError('late scheduler failure'))
assert commit.commit.call_count == 1
writer.abort.assert_not_called()
assert not any(name.startswith(('pypaimon.execution.python', 'pypaimon.execution.native', 'pypaimon_rust'))
for name in sys.modules)
'''
result = subprocess.run(
[sys.executable, '-c', source],
cwd=str(Path(__file__).resolve().parents[3]),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr