Skip to content

Commit 5d2bbe5

Browse files
committed
Add RCA for major freeze not allowed
1 parent e912c58 commit 5d2bbe5

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python
2+
# -*- coding: UTF-8 -*-
3+
# Copyright (c) 2022 OceanBase
4+
# OceanBase Diagnostic Tool is licensed under Mulan PSL v2.
5+
# You can use this software according to the terms and conditions of the Mulan PSL v2.
6+
# You may obtain a copy of Mulan PSL v2 at:
7+
# http://license.coscl.org.cn/MulanPSL2
8+
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
9+
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
10+
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
11+
# See the Mulan PSL v2 for more details.
12+
13+
"""
14+
@time: 2026/6/8
15+
@file: major_freeze_not_allow.py
16+
@desc: RCA scene for manual major freeze rejected by OB_MAJOR_FREEZE_NOT_ALLOW.
17+
"""
18+
19+
import os
20+
21+
from src.handler.rca.rca_exception import RCAExecuteException, RCAInitException
22+
from src.handler.rca.rca_handler import RcaScene
23+
from src.common.tool import StringUtils
24+
25+
26+
class MajorFreezeNotAllowScene(RcaScene):
27+
ERROR_PATTERNS = ("OB_MAJOR_FREEZE_NOT_ALLOW", "ERROR 4217", "-4217", "MAJOR FREEZE NOT ALLOWED NOW")
28+
29+
def __init__(self):
30+
super().__init__()
31+
self.tenant_name = None
32+
self.error_msg = None
33+
self.collect_logs = True
34+
35+
def verbose(self, info):
36+
self.stdio.verbose("[MajorFreezeNotAllowScene] {0}".format(info))
37+
38+
def get_scene_info(self):
39+
return {
40+
"name": "major_freeze_not_allow",
41+
"info_en": "Diagnose OB_MAJOR_FREEZE_NOT_ALLOW when manually executing major freeze.",
42+
"info_cn": "诊断手动执行合并时出现 OB_MAJOR_FREEZE_NOT_ALLOW 的原因",
43+
"example": "obdiag rca run --scene=major_freeze_not_allow --env tenant_name=xxx [--env error_msg='ERROR 4217 (HY000): Major freeze not allowed now']",
44+
}
45+
46+
def init(self, context):
47+
super().init(context)
48+
min_supported_version = "4.0.0.0"
49+
if self.observer_version is None or len(self.observer_version.strip()) == 0:
50+
raise RCAInitException("observer version is None. Please check the NODES conf.")
51+
if not (self.observer_version == min_supported_version or StringUtils.compare_versions_greater(self.observer_version, min_supported_version)):
52+
raise RCAInitException("observer version is {0}, which is less than {1}.".format(self.observer_version, min_supported_version))
53+
if self.ob_connector is None:
54+
raise RCAInitException("ob_connector is None. Please check the NODES conf.")
55+
56+
self.tenant_name = self.input_parameters.get("tenant_name")
57+
if not self.tenant_name:
58+
raise RCAInitException("tenant_name is required. Please use --env tenant_name=xxx.")
59+
60+
self.error_msg = self.input_parameters.get("error_msg")
61+
collect_logs = str(self.input_parameters.get("collect_logs", "true")).lower()
62+
self.collect_logs = collect_logs not in ("0", "false", "no")
63+
self.work_path = context.get_variable("store_dir")
64+
if not os.path.exists(self.work_path):
65+
os.makedirs(self.work_path)
66+
self.record.add_record("major_freeze_not_allow init: tenant_name={0}, collect_logs={1}".format(self.tenant_name, self.collect_logs))
67+
68+
@classmethod
69+
def is_major_freeze_not_allow_error(cls, error_msg):
70+
if not error_msg:
71+
return True
72+
normalized_error_msg = str(error_msg).upper()
73+
return any(pattern in normalized_error_msg for pattern in cls.ERROR_PATTERNS)
74+
75+
@staticmethod
76+
def _escape_sql_literal(value):
77+
return str(value).replace("'", "''")
78+
79+
def _query_tenant_role(self, tenant_name):
80+
tenant_name_escaped = self._escape_sql_literal(tenant_name)
81+
sql = "SELECT tenant_name, tenant_role FROM oceanbase.DBA_OB_TENANTS WHERE tenant_name = '{0}'".format(tenant_name_escaped)
82+
self.record.add_record("Query tenant role: {0}".format(sql))
83+
cursor = self.ob_connector.execute_sql_return_cursor_dictionary(sql)
84+
rows = cursor.fetchall()
85+
if not rows:
86+
return None
87+
return rows[0]
88+
89+
@staticmethod
90+
def _row_get(row, *keys):
91+
for key in keys:
92+
if key in row:
93+
return row.get(key)
94+
return None
95+
96+
def _check_tenant_role(self):
97+
row = self._query_tenant_role(self.tenant_name)
98+
if row is None:
99+
self.record.add_record("Tenant '{0}' was not found in oceanbase.DBA_OB_TENANTS.".format(self.tenant_name))
100+
self.record.add_suggest("Check whether tenant_name is correct, then rerun this RCA scene.")
101+
return
102+
103+
tenant_role = self._row_get(row, "tenant_role", "TENANT_ROLE")
104+
tenant_name = self._row_get(row, "tenant_name", "TENANT_NAME") or self.tenant_name
105+
self.record.add_record("Tenant role: tenant_name={0}, tenant_role={1}".format(tenant_name, tenant_role))
106+
if str(tenant_role).upper() == "STANDBY":
107+
self.record.add_suggest("Tenant '{0}' is a standby tenant. Manual `ALTER SYSTEM MAJOR FREEZE tenant = {0}` is not allowed on standby tenants; execute manual major freeze on the primary tenant or wait for synchronization.".format(tenant_name))
108+
else:
109+
self.record.add_suggest("Tenant '{0}' role is {1}, not STANDBY. OB_MAJOR_FREEZE_NOT_ALLOW is not explained by standby tenant role; continue checking observer/rootservice logs around the failed major freeze.".format(tenant_name, tenant_role))
110+
111+
def _gather_related_logs(self):
112+
if not self.collect_logs or self.gather_log is None:
113+
self.record.add_record("Skip collecting logs for OB_MAJOR_FREEZE_NOT_ALLOW.")
114+
return
115+
log_path = os.path.join(self.work_path, "major_freeze_not_allow_logs")
116+
self.gather_log.set_parameters("scope", "observer")
117+
self.gather_log.grep("OB_MAJOR_FREEZE_NOT_ALLOW")
118+
logs_name = self.gather_log.execute(save_path=log_path)
119+
if logs_name:
120+
self.record.add_record("Collected OB_MAJOR_FREEZE_NOT_ALLOW logs: {0}".format(logs_name))
121+
else:
122+
self.record.add_record("No OB_MAJOR_FREEZE_NOT_ALLOW logs were collected.")
123+
124+
def execute(self):
125+
try:
126+
if not self.is_major_freeze_not_allow_error(self.error_msg):
127+
self.record.add_record("error_msg does not contain OB_MAJOR_FREEZE_NOT_ALLOW patterns: {0}".format(self.error_msg))
128+
self.record.add_suggest("This scene is intended for ERROR 4217 / OB_MAJOR_FREEZE_NOT_ALLOW. Please check the error message or choose another RCA scene.")
129+
return
130+
self.record.add_record("Start diagnosing OB_MAJOR_FREEZE_NOT_ALLOW for tenant '{0}'.".format(self.tenant_name))
131+
self._gather_related_logs()
132+
self._check_tenant_role()
133+
except Exception as e:
134+
raise RCAExecuteException("MajorFreezeNotAllowScene execute error: {0}".format(e))
135+
finally:
136+
self.stdio.verbose("end MajorFreezeNotAllowScene execute")
137+
138+
139+
major_freeze_not_allow = MajorFreezeNotAllowScene()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python
2+
# -*- coding: UTF-8 -*-
3+
# Copyright (c) 2022 OceanBase
4+
# OceanBase Diagnostic Tool is licensed under Mulan PSL v2.
5+
# You can use this software according to the terms and conditions of the Mulan PSL v2.
6+
# You may obtain a copy of Mulan PSL v2 at:
7+
# http://license.coscl.org.cn/MulanPSL2
8+
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
9+
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
10+
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
11+
# See the Mulan PSL v2 for more details.
12+
13+
"""
14+
@time: 2026/6/8
15+
@file: test_major_freeze_not_allow.py
16+
@desc:
17+
"""
18+
19+
import unittest
20+
from unittest.mock import MagicMock
21+
22+
from plugins.rca.major_freeze_not_allow import MajorFreezeNotAllowScene
23+
from src.handler.rca.rca_handler import RCA_ResultRecord
24+
25+
26+
class TestMajorFreezeNotAllowScene(unittest.TestCase):
27+
def setUp(self):
28+
self.scene = MajorFreezeNotAllowScene()
29+
self.scene.tenant_name = "standby_tenant"
30+
self.scene.record = RCA_ResultRecord()
31+
self.scene.ob_connector = MagicMock()
32+
33+
def _set_query_rows(self, rows):
34+
cursor = MagicMock()
35+
cursor.fetchall.return_value = rows
36+
self.scene.ob_connector.execute_sql_return_cursor_dictionary.return_value = cursor
37+
38+
def test_error_pattern_matches_4217(self):
39+
self.assertTrue(self.scene.is_major_freeze_not_allow_error("ERROR 4217 (HY000): Major freeze not allowed now"))
40+
self.assertTrue(self.scene.is_major_freeze_not_allow_error("ret=-4217 OB_MAJOR_FREEZE_NOT_ALLOW"))
41+
self.assertFalse(self.scene.is_major_freeze_not_allow_error("ERROR 4002: lock wait timeout"))
42+
43+
def test_standby_tenant_role_adds_expected_suggestion(self):
44+
self._set_query_rows([{"tenant_name": "standby_tenant", "tenant_role": "STANDBY"}])
45+
self.scene._check_tenant_role()
46+
self.assertIn("standby tenant", self.scene.record.suggest)
47+
self.assertIn("not allowed", self.scene.record.suggest)
48+
49+
def test_primary_tenant_role_does_not_blame_standby(self):
50+
self._set_query_rows([{"TENANT_NAME": "primary_tenant", "TENANT_ROLE": "PRIMARY"}])
51+
self.scene.tenant_name = "primary_tenant"
52+
self.scene._check_tenant_role()
53+
self.assertIn("not STANDBY", self.scene.record.suggest)
54+
55+
def test_missing_tenant_adds_clear_suggestion(self):
56+
self._set_query_rows([])
57+
self.scene._check_tenant_role()
58+
self.assertIn("tenant_name is correct", self.scene.record.suggest)
59+
60+
def test_tenant_name_is_escaped_in_sql(self):
61+
self._set_query_rows([{"tenant_name": "tenant'1", "tenant_role": "STANDBY"}])
62+
self.scene.tenant_name = "tenant'1"
63+
self.scene._check_tenant_role()
64+
sql = self.scene.ob_connector.execute_sql_return_cursor_dictionary.call_args[0][0]
65+
self.assertIn("tenant''1", sql)
66+
67+
68+
if __name__ == '__main__':
69+
unittest.main()

0 commit comments

Comments
 (0)