blob: 7fd61d8911efddb011cb93b1917d582a5df4c730 [file] [log] [blame]
# 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.
from tests.common.custom_cluster_test_suite import CustomClusterTestSuite
import pytest
import re
import shutil
import tempfile
from beeswaxd.BeeswaxService import QueryState
from tests.common.skip import SkipIfNotHdfsMinicluster
from tests.common.skip import SkipIfBuildType
from time import sleep
# The BE krpc port of the impalad to simulate disk errors in tests.
FAILED_KRPC_PORT = 27001
def _get_disk_write_fail_action(port):
return "IMPALA_TMP_FILE_WRITE:127.0.0.1:{port}:FAIL".format(port=port)
# Tests that verify the behavior of the executor blacklist caused by RPC failure.
# Coordinator adds an executor node to its blacklist if the RPC to that node failed.
# Note: query-retry is not enabled by default.
@SkipIfNotHdfsMinicluster.tuned_for_minicluster
class TestBlacklist(CustomClusterTestSuite):
@classmethod
def get_workload(cls):
return 'functional-query'
@classmethod
def setup_class(cls):
if cls.exploration_strategy() != 'exhaustive':
pytest.skip('runs only in exhaustive')
super(TestBlacklist, cls).setup_class()
@pytest.mark.execute_serially
@CustomClusterTestSuite.with_args(
statestored_args="-statestore_heartbeat_frequency_ms=1000")
def test_kill_impalad(self, cursor):
"""Test that verifies that when an impalad is killed, it is properly blacklisted."""
# Run a query and verify that no impalads are blacklisted yet.
result = self.execute_query("select count(*) from tpch.lineitem")
assert re.search("Blacklisted Executors: (.*)", result.runtime_profile) is None, \
result.runtime_profile
# Kill an impalad
killed_impalad = self.cluster.impalads[2]
killed_impalad.kill()
# Run a query which should fail as the impalad hasn't been blacklisted yet.
try:
self.execute_query("select count(*) from tpch.lineitem")
assert False, "Query was expected to fail"
except Exception as e:
assert "Exec() rpc failed" in str(e)
# Run another query which should succeed and verify the impalad was blacklisted.
result = self.execute_query("select count(*) from tpch.lineitem")
backends_json = self.cluster.impalads[0].service.get_debug_webpage_json("/backends")
match = re.search("Blacklisted Executors: (.*)", result.runtime_profile)
assert match.group(1) == "%s:%s" % \
(killed_impalad.hostname, killed_impalad.service.krpc_port), \
result.runtime_profile
assert backends_json["num_blacklisted_backends"] == 1, backends_json
assert backends_json["num_active_backends"] == 2, backends_json
assert len(backends_json["backends"]) == 3, backends_json
num_blacklisted = 0
for backend_json in backends_json["backends"]:
if str(killed_impalad.service.krpc_port) in backend_json["krpc_address"]:
assert backend_json["is_blacklisted"], backend_json
num_blacklisted += 1
else:
assert not backend_json["is_blacklisted"], backend_json
assert num_blacklisted == 1, backends_json
# Sleep for long enough for the statestore to remove the impalad from the cluster
# membership, i.e. Statestore::FailedExecutorDetectionTime() + some padding
sleep(12)
# Run another query and verify nothing was blacklisted and only 2 backends were
# scheduled on.
result = self.execute_query("select count(*) from tpch.lineitem")
assert re.search("Blacklisted Executors: (.*)", result.runtime_profile) is None, \
result.runtime_profile
assert re.search("NumBackends: 2", result.runtime_profile), result.runtime_profile
@pytest.mark.execute_serially
def test_restart_impalad(self, cursor):
"""Test that verifies the behavior when an impalad is killed, blacklisted, and then
restarted."""
# Run a query and verify that no impalads are blacklisted yet.
result = self.execute_query("select count(*) from tpch.lineitem")
assert re.search("Blacklisted Executors: (.*)", result.runtime_profile) is None, \
result.runtime_profile
# Kill an impalad
killed_impalad = self.cluster.impalads[2]
killed_impalad.kill()
# Run a query which should fail as the impalad hasn't been blacklisted yet.
try:
self.execute_query("select count(*) from tpch.lineitem")
assert False, "Query was expected to fail"
except Exception as e:
assert "Exec() rpc failed" in str(e)
# Run another query which should succeed and verify the impalad was blacklisted.
result = self.execute_query("select count(*) from tpch.lineitem")
match = re.search("Blacklisted Executors: (.*)", result.runtime_profile)
assert match.group(1) == "%s:%s" % \
(killed_impalad.hostname, killed_impalad.service.krpc_port), \
result.runtime_profile
# Restart the impalad.
killed_impalad.start()
# Sleep for long enough for the statestore to update the membership to include the
# restarted impalad, ImpaladProcess.start() won't return until the Impalad says its
# ready to accept connections, at which point it will have already registered with the
# statestore, so we don't need to sleep very long.
sleep(2)
# Run another query and verify nothing was blacklisted and all 3 backends were
# scheduled on.
result = self.execute_query("select count(*) from tpch.lineitem")
assert re.search("Blacklisted Executors: (.*)", result.runtime_profile) is None, \
result.runtime_profile
assert re.search("NumBackends: 3", result.runtime_profile), result.runtime_profile
@pytest.mark.execute_serially
@CustomClusterTestSuite.with_args(num_exclusive_coordinators=1,
statestored_args="-statestore_heartbeat_frequency_ms=1000")
def test_kill_impalad_with_running_queries(self, cursor):
"""Verifies that when an Impala executor is killed while running a query, that the
Coordinator blacklists the killed executor."""
# Run a query asynchronously. With the debug actions, this query should take a few
# minutes to complete.
query = "select count(*) from tpch_parquet.lineitem t1, tpch_parquet.lineitem t2 \
where t1.l_orderkey = t2.l_orderkey"
handle = self.execute_query_async(query, query_options={
'debug_action': '0:GETNEXT:DELAY|1:GETNEXT:DELAY'})
# Wait for the query to start running
self.wait_for_any_state(handle, [QueryState.RUNNING, QueryState.FINISHED], 10)
# Kill one of the Impala executors
killed_impalad = self.cluster.impalads[2]
killed_impalad.kill()
# Try to fetch results from the query. Fetch requests should fail because one of the
# impalads running the query was killed. When the query fails, the Coordinator should
# add the killed Impala executor to the blacklist (since a RPC to that node failed).
try:
self.client.fetch(query, handle)
assert False, "Query was expected to fail"
except Exception as e:
# The query should fail due to an RPC error.
assert "TransmitData() to " in str(e) or "EndDataStream() to " in str(e)
# Run another query which should succeed and verify the impalad was blacklisted.
self.client.clear_configuration() # remove the debug actions
result = self.execute_query("select count(*) from tpch.lineitem")
match = re.search("Blacklisted Executors: (.*)", result.runtime_profile)
assert match is not None and match.group(1) == "%s:%s" % \
(killed_impalad.hostname, killed_impalad.service.krpc_port), \
result.runtime_profile
# Tests that verify the behavior of the executor blacklist caused by disk IO failure.
# Coordinator adds an executor node to its blacklist if that node reported query
# execution status with error caused by its local faulty disk.
# Note: query-retry is not enabled by default and we assume it's not enabled in following
# tests.
@SkipIfNotHdfsMinicluster.tuned_for_minicluster
class TestBlacklistFaultyDisk(CustomClusterTestSuite):
@classmethod
def get_workload(cls):
return 'functional-query'
@classmethod
def setup_class(cls):
if cls.exploration_strategy() != 'exhaustive':
pytest.skip('runs only in exhaustive')
super(TestBlacklistFaultyDisk, cls).setup_class()
# Query with order by requires spill to disk if intermediate results don't fit in mem
spill_query = """
select o_orderdate, o_custkey, o_comment
from tpch.orders
order by o_orderdate
"""
# Query against a big table with order by requires spill to disk if intermediate
# results don't fit in memory.
spill_query_big_table = """
select l_orderkey, l_linestatus, l_shipdate, l_comment
from tpch.lineitem
order by l_orderkey
"""
# Query without order by can be executed without spilling to disk.
in_mem_query = """
select o_orderdate, o_custkey, o_comment from tpch.orders
"""
# Buffer pool limit that is low enough to force Impala to spill to disk when executing
# spill_query.
buffer_pool_limit = "45m"
def __generate_scratch_dir(self, num):
result = []
for i in xrange(num):
dir_path = tempfile.mkdtemp()
self.created_dirs.append(dir_path)
result.append(dir_path)
print "Generated dir" + dir_path
return result
def setup_method(self, method):
# Don't call the superclass method to prevent starting Impala before each test. In
# this class, each test is responsible for doing that because we want to generate
# the parameter string to start-impala-cluster in each test method.
self.created_dirs = []
def teardown_method(self, method):
for dir_path in self.created_dirs:
shutil.rmtree(dir_path, ignore_errors=True)
@SkipIfBuildType.not_dev_build
@pytest.mark.execute_serially
def test_scratch_file_write_failure(self, vector):
""" Test that verifies that when an impalad failed to execute query during spill-to-
disk due to disk write error, it is properly blacklisted by coordinator."""
# Start cluster with spill-to-disk enabled and one dedicated coordinator. Set a high
# statestore heartbeat frequency so that blacklisted nodes are not timeout too
# quickly.
scratch_dirs = self.__generate_scratch_dir(2)
self._start_impala_cluster([
'--impalad_args=-logbuflevel=-1',
'--impalad_args=--scratch_dirs={0}'.format(','.join(scratch_dirs)),
'--impalad_args=--allow_multiple_scratch_dirs_per_device=true',
'--impalad_args=--statestore_heartbeat_frequency_ms=2000',
'--cluster_size=3', '--num_coordinators=1', '--use_exclusive_coordinators'])
self.assert_impalad_log_contains("INFO", "Using scratch directory ",
expected_count=2)
# First set debug_action for query as empty.
vector.get_value('exec_option')['buffer_pool_limit'] = self.buffer_pool_limit
vector.get_value('exec_option')['debug_action'] = ''
coord_impalad = self.cluster.get_first_impalad()
client = coord_impalad.service.create_beeswax_client()
# Expect spill to disk to success with debug_action as empty. Verify all nodes are
# active.
handle = self.execute_query_async_using_client(client, self.spill_query, vector)
results = client.fetch(self.spill_query, handle)
assert results.success
client.close_query(handle)
backends_json = coord_impalad.service.get_debug_webpage_json("/backends")
assert backends_json["num_active_backends"] == 3, backends_json
assert len(backends_json["backends"]) == 3, backends_json
# Set debug_action to inject disk write error for spill-to-disk on impalad for which
# krpc port is 27001.
vector.get_value('exec_option')['debug_action'] = \
_get_disk_write_fail_action(FAILED_KRPC_PORT)
# Should be able to execute in-memory query.
handle = self.execute_query_async_using_client(client, self.in_mem_query, vector)
results = client.fetch(self.in_mem_query, handle)
assert results.success
client.close_query(handle)
# Expect spill to disk to fail due to disk failure on the impalad with disk failure.
# Verify one node is blacklisted.
disk_failure_impalad = self.cluster.impalads[1]
assert disk_failure_impalad.service.krpc_port == FAILED_KRPC_PORT
try:
handle = self.execute_query_async_using_client(client, self.spill_query, vector)
results = client.fetch(self.spill_query, handle)
assert False, "Query was expected to fail"
except Exception as e:
assert "Query execution failure caused by local disk IO fatal error on backend" \
in str(e)
backends_json = coord_impalad.service.get_debug_webpage_json("/backends")
assert backends_json["num_blacklisted_backends"] == 1, backends_json
assert backends_json["num_active_backends"] == 2, backends_json
assert len(backends_json["backends"]) == 3, backends_json
num_blacklisted = 0
for backend_json in backends_json["backends"]:
if str(disk_failure_impalad.service.krpc_port) in backend_json["krpc_address"]:
assert backend_json["is_blacklisted"], backend_json
assert "Query execution failure caused by local disk IO fatal error on backend" \
in backend_json["blacklist_cause"]
num_blacklisted += 1
else:
assert not backend_json["is_blacklisted"], backend_json
assert num_blacklisted == 1, backends_json
# Should be able to re-execute same query since the impalad with injected disk error
# for spill-to-disk was blacklisted.
handle = self.execute_query_async_using_client(client, self.spill_query, vector)
results = client.fetch(self.spill_query, handle)
assert results.success
# Verify that the runtime profile contains the "Blacklisted Executors" line with the
# corresponding backend.
runtime_profile = client.get_runtime_profile(handle)
match = re.search("Blacklisted Executors: (.*)", runtime_profile)
assert match is not None and match.group(1) == "%s:%s" % \
(disk_failure_impalad.hostname, disk_failure_impalad.service.krpc_port), \
runtime_profile
client.close_query(handle)
# Sleep for long enough time and verify blacklisted backend was removed from the
# blacklist after blacklisting timeout.
sleep(30)
backends_json = coord_impalad.service.get_debug_webpage_json("/backends")
assert backends_json["num_active_backends"] == 3, backends_json
# Run the query again without the debug action and verify nothing was blacklisted
# and all 3 backends were scheduled on.
vector.get_value('exec_option')['debug_action'] = ''
handle = self.execute_query_async_using_client(client, self.spill_query, vector)
results = client.fetch(self.spill_query, handle)
assert results.success
runtime_profile = client.get_runtime_profile(handle)
assert re.search("Blacklisted Executors: (.*)", runtime_profile) is None, \
runtime_profile
assert re.search("NumBackends: 3", runtime_profile), runtime_profile
client.close_query(handle)
client.close()