Skip to content

Commit 9a922e2

Browse files
authored
Merge pull request #172 from tanmoysrt/post_restore_validation
fix(physical-restore): Validate and fix tables after restoration
2 parents e8cd19f + ebf1c0c commit 9a922e2

File tree

1 file changed

+122
-0
lines changed

1 file changed

+122
-0
lines changed

agent/database_physical_restore.py

+122
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def create_restore_job(self):
7272
self.import_tablespaces_in_target_db()
7373
self.hold_write_lock_on_myisam_tables()
7474
self.perform_myisam_file_operations()
75+
self.perform_post_restoration_validation_and_fixes()
7576
self.unlock_all_tables()
7677

7778
@step("Validate Backup Files")
@@ -277,6 +278,46 @@ def hold_write_lock_on_myisam_tables(self):
277278
def perform_myisam_file_operations(self):
278279
self._perform_file_operations(engine="myisam")
279280

281+
@step("Validate And Fix Tables")
282+
def perform_post_restoration_validation_and_fixes(self):
283+
innodb_tables_with_fts = self.get_innodb_tables_with_fts_index()
284+
"""
285+
FLUSH TABLES ... FOR EXPORT does not support FULLTEXT indexes.
286+
https://dev.mysql.com/doc/refman/8.4/en/innodb-table-import.html#:~:text=in%20the%20operation.-,Limitations,-The%20Transportable%20Tablespaces
287+
288+
We can either drop + add index.
289+
Or, run `OPTIMIZE TABLE` on the table to rebuild the index.
290+
https://mariadb.com/kb/en/optimize-table/#updating-an-innodb-fulltext-index
291+
"""
292+
293+
for table in innodb_tables_with_fts:
294+
if self.is_table_corrupted(table) and not self.repair_table(table, "innodb"):
295+
raise Exception(f"Failed to repair table {table}")
296+
297+
"""
298+
MyISAM table corruption can generally happen due to mismatch of no of records in MYD file.
299+
300+
myisamchk can't find and fix this issue.
301+
Because this out of sync happen after creating a blank MyISAM table and just copying MYF & MYI files.
302+
303+
Usually, DB Restart will fix this issue. But we can't do in live database.
304+
So running `REPAIR TABLE ... USE_FRM` can fix the issue.
305+
https://dev.mysql.com/doc/refman/8.4/en/myisam-repair.html
306+
"""
307+
for table in self.myisam_tables:
308+
if self.is_table_corrupted(table) and not self.repair_table(table, "myisam"):
309+
raise Exception(f"Failed to repair table {table}")
310+
311+
for table in self.innodb_tables:
312+
if table in innodb_tables_with_fts:
313+
continue
314+
"""
315+
If other innodb tables are corrupted,
316+
We can't repair the table in running database
317+
"""
318+
if self.is_table_corrupted(table):
319+
raise Exception(f"Failed to repair table {table}")
320+
280321
@step("Unlock All Tables")
281322
def unlock_all_tables(self):
282323
self._get_target_db().execute_sql("UNLOCK TABLES;")
@@ -388,8 +429,89 @@ def get_drop_table_statement(self, table_name) -> str:
388429

389430
return f"DROP TABLE IF EXISTS `{table_name}`;"
390431

432+
def is_table_corrupted(self, table_name: str) -> bool:
433+
result = run_sql_query(self._get_target_db(), f"CHECK TABLE `{table_name}` QUICK;")
434+
"""
435+
+-----------------------------------+-------+----------+------------------------------------------------------+
436+
| Table | Op | Msg_type | Msg_text |
437+
+-----------------------------------+-------+----------+------------------------------------------------------+
438+
| _8edd549f4b072174.__global_search | check | warning | Size of indexfile is: 22218752 Should be: 4096 |
439+
| _8edd549f4b072174.__global_search | check | warning | Size of datafile is: 31303496 Should be: 0 |
440+
| _8edd549f4b072174.__global_search | check | error | Record-count is not ok; is 152774 Should be: 0 |
441+
| _8edd549f4b072174.__global_search | check | warning | Found 172605 key parts. Should be: 0 |
442+
| _8edd549f4b072174.__global_search | check | error | Corrupt |
443+
+-----------------------------------+-------+----------+------------------------------------------------------+
444+
445+
+-------------------------------------------+-------+----------+--------------------------------------------------------+
446+
| Table | Op | Msg_type | Msg_text |
447+
+-------------------------------------------+-------+----------+--------------------------------------------------------+
448+
| _8edd549f4b072174.energy_point_log_id_seq | check | note | The storage engine for the table doesn't support check |
449+
+-------------------------------------------+-------+----------+--------------------------------------------------------+
450+
""" # noqa: E501
451+
isError = False
452+
for row in result:
453+
if row[2] == "error":
454+
isError = True
455+
break
456+
return isError
457+
458+
def repair_table(self, table_name: str, engine: str) -> bool:
459+
if engine == "innodb":
460+
result = run_sql_query(self._get_target_db(), f"OPTIMIZE TABLE `{table_name}`;")
461+
elif engine == "myisam":
462+
result = run_sql_query(self._get_target_db(), f"REPAIR TABLE `{table_name}` USE_FRM;")
463+
else:
464+
raise Exception(f"Engine {engine} is not supported")
465+
"""
466+
+---------------------------------------------------+--------+----------+----------+
467+
| Table | Op | Msg_type | Msg_text |
468+
+---------------------------------------------------+--------+----------+----------+
469+
| _8edd549f4b072174.tabInsights Query Execution Log | repair | status | OK |
470+
+---------------------------------------------------+--------+----------+----------+
471+
472+
Msg Type can be status, error, info, note, or warning
473+
"""
474+
isErrorOccurred = False
475+
for row in result:
476+
if row[2] == "error":
477+
isErrorOccurred = True
478+
break
479+
480+
return not isErrorOccurred
481+
482+
def get_innodb_tables_with_fts_index(self):
483+
rows = run_sql_query(
484+
self._get_target_db(),
485+
f"""
486+
SELECT
487+
DISTINCT(t.TABLE_NAME)
488+
FROM
489+
information_schema.STATISTICS s
490+
JOIN
491+
information_schema.TABLES t
492+
ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
493+
AND s.TABLE_NAME = t.TABLE_NAME
494+
WHERE
495+
s.INDEX_TYPE = 'FULLTEXT'
496+
AND t.TABLE_SCHEMA = '{self.target_db}'
497+
AND t.ENGINE = 'InnoDB'
498+
""",
499+
)
500+
return [row[0] for row in rows]
501+
391502
def __del__(self):
392503
if self._target_db_instance is not None:
393504
self._target_db_instance.close()
394505
if self._target_db_instance_for_myisam is not None:
395506
self._target_db_instance_for_myisam.close()
507+
508+
509+
def run_sql_query(db: CustomPeeweeDB, query: str) -> list[str]:
510+
"""
511+
Return the result of the query as a list of rows
512+
"""
513+
cursor = db.execute_sql(query)
514+
if not cursor.description:
515+
return []
516+
rows = cursor.fetchall()
517+
return [row for row in rows]

0 commit comments

Comments
 (0)