blob: 74dcfcc015ee9dab33d92efe5bd6678a72afefdb [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.
"""Both commit executors implement the same publication and lifecycle protocol."""
import inspect
from unittest.mock import Mock
import pytest
from pypaimon.execution.native.write.commit import NativeTableCommit
from pypaimon.execution.python.write.table_commit import PythonTableCommit
from pypaimon.snapshot.snapshot import BATCH_COMMIT_IDENTIFIER
from pypaimon.write.commit_message import CommitMessage
from pypaimon.write.table_commit import BatchTableCommit
@pytest.mark.parametrize('method', [
'commit', 'abort', 'truncate_table', 'truncate_partitions', 'close',
])
def test_commit_executors_share_operation_protocol(method):
assert inspect.signature(getattr(PythonTableCommit, method)) == inspect.signature(
getattr(NativeTableCommit, method))
def _executor(backend, core, overwrite=None):
if backend == 'native':
return NativeTableCommit(core, is_batch=overwrite is not None)
return PythonTableCommit(core, overwrite_partition=overwrite)
@pytest.mark.parametrize('backend', ['python', 'native'])
@pytest.mark.parametrize('overwrite', [None, {}, {'pt': 'a'}])
@pytest.mark.parametrize('properties', [None, {'source': 'protocol'}])
def test_commit_executor_preserves_scope_messages_identifier_and_properties(
backend, overwrite, properties):
core = Mock()
executor = _executor(backend, core, overwrite)
messages = [object()]
identifier = BATCH_COMMIT_IDENTIFIER if overwrite is not None else 17
executor.commit(messages, identifier, properties)
kwargs = {} if properties is None else {'snapshot_properties': properties}
if backend == 'native':
if overwrite is None:
core.commit.assert_called_once_with(identifier, messages, **kwargs)
else:
core.commit.assert_called_once_with(messages, **kwargs)
else:
kwargs.update(commit_messages=messages, commit_identifier=identifier)
if overwrite is None:
core.commit.assert_called_once_with(**kwargs)
core.overwrite.assert_not_called()
else:
core.overwrite.assert_called_once_with(overwrite_partition=overwrite, **kwargs)
core.commit.assert_not_called()
core.abort.assert_not_called()
@pytest.mark.parametrize('backend', ['python', 'native'])
@pytest.mark.parametrize('overwrite', [None, {}])
def test_commit_executor_preserves_prepared_files_when_publication_outcome_is_unknown(
tmp_path, backend, overwrite):
prepared = tmp_path / 'prepared.parquet'
prepared.write_bytes(b'prepared')
failure = TimeoutError('publication response was lost')
core = Mock()
getattr(core, 'overwrite' if backend == 'python' and overwrite is not None else 'commit').side_effect = failure
executor = _executor(backend, core, overwrite)
with pytest.raises(TimeoutError) as caught:
executor.commit([object()], BATCH_COMMIT_IDENTIFIER)
assert caught.value is failure
assert prepared.read_bytes() == b'prepared'
core.abort.assert_not_called()
@pytest.mark.parametrize('backend', ['python', 'native'])
def test_partition_truncate_does_not_consume_the_append_commit(backend):
core = Mock()
executor = (NativeTableCommit(core, is_batch=True) if backend == 'native'
else PythonTableCommit(core))
partitions = [{'pt': 'a'}]
executor.truncate_partitions(partitions, BATCH_COMMIT_IDENTIFIER)
executor.commit([], BATCH_COMMIT_IDENTIFIER)
if backend == 'native':
core.truncate_partitions.assert_called_once_with(partitions)
core.commit.assert_called_once_with([])
else:
core.drop_partitions.assert_called_once_with(partitions, BATCH_COMMIT_IDENTIFIER)
core.commit.assert_called_once_with(commit_messages=[], commit_identifier=BATCH_COMMIT_IDENTIFIER)
@pytest.mark.parametrize('backend', ['python', 'native'])
@pytest.mark.parametrize('failure', [False, True])
def test_public_commit_calls_one_executor_protocol_without_replay(backend, failure):
options = Mock()
options.native_commit_enabled.return_value = backend == 'native'
options.snapshot_ignore_empty_commit.return_value = False
table = Mock(options=options, identifier='default.test')
owner = BatchTableCommit(table, 'user', None)
executor = Mock()
message = CommitMessage((), 0, [], deleted_files=[Mock()])
messages = [message]
prepared = messages if backend == 'python' else ['wire-message']
owner._prepare_native_commit = Mock(return_value=(executor, prepared) if backend == 'native' else None)
if backend == 'python':
owner._python_commit = executor
if failure:
executor.commit.side_effect = TimeoutError('outcome unknown')
with pytest.raises(TimeoutError, match='outcome unknown'):
owner.commit(messages)
else:
owner.commit(messages, snapshot_properties={'checkpoint': 'batch'})
executor.commit.assert_called_once_with(
prepared, BATCH_COMMIT_IDENTIFIER,
snapshot_properties=None if failure else {'checkpoint': 'batch'})
table.new_snapshot_commit.assert_not_called()
executor.abort.assert_not_called()
with pytest.raises(RuntimeError, match='one-time'):
owner.commit(messages)