starrocks-br 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- starrocks_br/cli.py +106 -57
- starrocks_br/concurrency.py +2 -9
- starrocks_br/config.py +11 -7
- starrocks_br/db.py +3 -3
- starrocks_br/error_handler.py +307 -0
- starrocks_br/exceptions.py +109 -0
- starrocks_br/executor.py +4 -4
- starrocks_br/history.py +4 -6
- starrocks_br/labels.py +2 -2
- starrocks_br/logger.py +45 -15
- starrocks_br/planner.py +5 -10
- starrocks_br/restore.py +13 -16
- starrocks_br/timezone.py +1 -2
- {starrocks_br-0.4.0.dist-info → starrocks_br-0.5.1.dist-info}/METADATA +3 -2
- starrocks_br-0.5.1.dist-info/RECORD +23 -0
- starrocks_br-0.4.0.dist-info/RECORD +0 -21
- {starrocks_br-0.4.0.dist-info → starrocks_br-0.5.1.dist-info}/WHEEL +0 -0
- {starrocks_br-0.4.0.dist-info → starrocks_br-0.5.1.dist-info}/entry_points.txt +0 -0
- {starrocks_br-0.4.0.dist-info → starrocks_br-0.5.1.dist-info}/top_level.txt +0 -0
starrocks_br/cli.py
CHANGED
|
@@ -6,6 +6,8 @@ import click
|
|
|
6
6
|
from . import (
|
|
7
7
|
concurrency,
|
|
8
8
|
db,
|
|
9
|
+
error_handler,
|
|
10
|
+
exceptions,
|
|
9
11
|
executor,
|
|
10
12
|
health,
|
|
11
13
|
labels,
|
|
@@ -63,9 +65,20 @@ def _handle_snapshot_exists_error(
|
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
@click.group()
|
|
66
|
-
|
|
68
|
+
@click.option("--verbose", is_flag=True, help="Enable verbose debug logging")
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def cli(ctx, verbose):
|
|
67
71
|
"""StarRocks Backup & Restore automation tool."""
|
|
68
|
-
|
|
72
|
+
ctx.ensure_object(dict)
|
|
73
|
+
ctx.obj["verbose"] = verbose
|
|
74
|
+
|
|
75
|
+
if verbose:
|
|
76
|
+
import logging
|
|
77
|
+
|
|
78
|
+
logger.setup_logging(level=logging.DEBUG)
|
|
79
|
+
logger.debug("Verbose logging enabled")
|
|
80
|
+
else:
|
|
81
|
+
logger.setup_logging()
|
|
69
82
|
|
|
70
83
|
|
|
71
84
|
@cli.command("init")
|
|
@@ -112,11 +125,14 @@ def init(config):
|
|
|
112
125
|
" starrocks-br backup incremental --group my_daily_incremental --config config.yaml"
|
|
113
126
|
)
|
|
114
127
|
|
|
115
|
-
except
|
|
116
|
-
|
|
128
|
+
except exceptions.ConfigFileNotFoundError as e:
|
|
129
|
+
error_handler.handle_config_file_not_found_error(e)
|
|
117
130
|
sys.exit(1)
|
|
118
|
-
except
|
|
119
|
-
|
|
131
|
+
except exceptions.ConfigValidationError as e:
|
|
132
|
+
error_handler.handle_config_validation_error(e, config)
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
except FileNotFoundError as e:
|
|
135
|
+
error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
|
|
120
136
|
sys.exit(1)
|
|
121
137
|
except Exception as e:
|
|
122
138
|
logger.error(f"Failed to initialize schema: {e}")
|
|
@@ -262,14 +278,26 @@ def backup_incremental(config, baseline_backup, group, name):
|
|
|
262
278
|
logger.error(f"{result['error_message']}")
|
|
263
279
|
sys.exit(1)
|
|
264
280
|
|
|
281
|
+
except exceptions.ConcurrencyConflictError as e:
|
|
282
|
+
error_handler.handle_concurrency_conflict_error(e, config)
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
except exceptions.BackupLabelNotFoundError as e:
|
|
285
|
+
error_handler.handle_backup_label_not_found_error(e, config)
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
except exceptions.NoFullBackupFoundError as e:
|
|
288
|
+
error_handler.handle_no_full_backup_found_error(e, config, group)
|
|
289
|
+
sys.exit(1)
|
|
290
|
+
except exceptions.ConfigFileNotFoundError as e:
|
|
291
|
+
error_handler.handle_config_file_not_found_error(e)
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
except exceptions.ConfigValidationError as e:
|
|
294
|
+
error_handler.handle_config_validation_error(e, config)
|
|
295
|
+
sys.exit(1)
|
|
265
296
|
except FileNotFoundError as e:
|
|
266
|
-
|
|
297
|
+
error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
|
|
267
298
|
sys.exit(1)
|
|
268
299
|
except ValueError as e:
|
|
269
|
-
logger.error(f"
|
|
270
|
-
sys.exit(1)
|
|
271
|
-
except RuntimeError as e:
|
|
272
|
-
logger.error(f"{e}")
|
|
300
|
+
logger.error(f"Error: {e}")
|
|
273
301
|
sys.exit(1)
|
|
274
302
|
except Exception as e:
|
|
275
303
|
logger.error(f"Unexpected error: {e}")
|
|
@@ -381,15 +409,23 @@ def backup_full(config, group, name):
|
|
|
381
409
|
logger.error(f"{result['error_message']}")
|
|
382
410
|
sys.exit(1)
|
|
383
411
|
|
|
384
|
-
except
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
412
|
+
except exceptions.ConcurrencyConflictError as e:
|
|
413
|
+
error_handler.handle_concurrency_conflict_error(e, config)
|
|
414
|
+
sys.exit(1)
|
|
415
|
+
except exceptions.ConfigFileNotFoundError as e:
|
|
416
|
+
error_handler.handle_config_file_not_found_error(e)
|
|
417
|
+
sys.exit(1)
|
|
418
|
+
except exceptions.ConfigValidationError as e:
|
|
419
|
+
error_handler.handle_config_validation_error(e, config)
|
|
420
|
+
sys.exit(1)
|
|
421
|
+
except FileNotFoundError as e:
|
|
422
|
+
error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
|
|
423
|
+
sys.exit(1)
|
|
424
|
+
except ValueError as e:
|
|
425
|
+
logger.error(f"Error: {e}")
|
|
426
|
+
sys.exit(1)
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logger.error(f"Unexpected error: {e}")
|
|
393
429
|
sys.exit(1)
|
|
394
430
|
|
|
395
431
|
|
|
@@ -429,14 +465,13 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
|
|
|
429
465
|
if table:
|
|
430
466
|
table = table.strip()
|
|
431
467
|
if not table:
|
|
432
|
-
|
|
433
|
-
sys.exit(1)
|
|
468
|
+
raise exceptions.InvalidTableNameError("", "Table name cannot be empty")
|
|
434
469
|
|
|
435
470
|
if "." in table:
|
|
436
|
-
|
|
437
|
-
|
|
471
|
+
raise exceptions.InvalidTableNameError(
|
|
472
|
+
table,
|
|
473
|
+
"Table name must not include database prefix. Use 'table_name' not 'database.table_name'",
|
|
438
474
|
)
|
|
439
|
-
sys.exit(1)
|
|
440
475
|
|
|
441
476
|
cfg = config_module.load_config(config)
|
|
442
477
|
config_module.validate_config(cfg)
|
|
@@ -472,39 +507,21 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
|
|
|
472
507
|
|
|
473
508
|
logger.info(f"Finding restore sequence for target backup: {target_label}")
|
|
474
509
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
logger.success(f"Found restore sequence: {' -> '.join(restore_pair)}")
|
|
478
|
-
except ValueError as e:
|
|
479
|
-
logger.error(f"Failed to find restore sequence: {e}")
|
|
480
|
-
sys.exit(1)
|
|
510
|
+
restore_pair = restore.find_restore_pair(database, target_label)
|
|
511
|
+
logger.success(f"Found restore sequence: {' -> '.join(restore_pair)}")
|
|
481
512
|
|
|
482
513
|
logger.info("Determining tables to restore from backup manifest...")
|
|
483
514
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
)
|
|
492
|
-
except ValueError as e:
|
|
493
|
-
logger.error(str(e))
|
|
494
|
-
sys.exit(1)
|
|
515
|
+
tables_to_restore = restore.get_tables_from_backup(
|
|
516
|
+
database,
|
|
517
|
+
target_label,
|
|
518
|
+
group=group,
|
|
519
|
+
table=table,
|
|
520
|
+
database=cfg["database"] if table else None,
|
|
521
|
+
)
|
|
495
522
|
|
|
496
523
|
if not tables_to_restore:
|
|
497
|
-
|
|
498
|
-
logger.warning(
|
|
499
|
-
f"No tables found in backup '{target_label}' for group '{group}'"
|
|
500
|
-
)
|
|
501
|
-
elif table:
|
|
502
|
-
logger.warning(
|
|
503
|
-
f"No tables found in backup '{target_label}' for table '{table}'"
|
|
504
|
-
)
|
|
505
|
-
else:
|
|
506
|
-
logger.warning(f"No tables found in backup '{target_label}'")
|
|
507
|
-
sys.exit(1)
|
|
524
|
+
raise exceptions.NoTablesFoundError(group=group, label=target_label)
|
|
508
525
|
|
|
509
526
|
logger.success(
|
|
510
527
|
f"Found {len(tables_to_restore)} table(s) to restore: {', '.join(tables_to_restore)}"
|
|
@@ -527,14 +544,46 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
|
|
|
527
544
|
logger.error(f"Restore failed: {result['error_message']}")
|
|
528
545
|
sys.exit(1)
|
|
529
546
|
|
|
547
|
+
except exceptions.InvalidTableNameError as e:
|
|
548
|
+
error_handler.handle_invalid_table_name_error(e)
|
|
549
|
+
sys.exit(1)
|
|
550
|
+
except exceptions.BackupLabelNotFoundError as e:
|
|
551
|
+
error_handler.handle_backup_label_not_found_error(e, config)
|
|
552
|
+
sys.exit(1)
|
|
553
|
+
except exceptions.NoSuccessfulFullBackupFoundError as e:
|
|
554
|
+
error_handler.handle_no_successful_full_backup_found_error(e, config)
|
|
555
|
+
sys.exit(1)
|
|
556
|
+
except exceptions.TableNotFoundInBackupError as e:
|
|
557
|
+
error_handler.handle_table_not_found_in_backup_error(e, config)
|
|
558
|
+
sys.exit(1)
|
|
559
|
+
except exceptions.NoTablesFoundError as e:
|
|
560
|
+
error_handler.handle_no_tables_found_error(e, config, target_label)
|
|
561
|
+
sys.exit(1)
|
|
562
|
+
except exceptions.SnapshotNotFoundError as e:
|
|
563
|
+
error_handler.handle_snapshot_not_found_error(e, config)
|
|
564
|
+
sys.exit(1)
|
|
565
|
+
except exceptions.RestoreOperationCancelledError:
|
|
566
|
+
error_handler.handle_restore_operation_cancelled_error()
|
|
567
|
+
sys.exit(1)
|
|
568
|
+
except exceptions.ConfigFileNotFoundError as e:
|
|
569
|
+
error_handler.handle_config_file_not_found_error(e)
|
|
570
|
+
sys.exit(1)
|
|
571
|
+
except exceptions.ConfigValidationError as e:
|
|
572
|
+
error_handler.handle_config_validation_error(e, config)
|
|
573
|
+
sys.exit(1)
|
|
574
|
+
except exceptions.ClusterHealthCheckFailedError as e:
|
|
575
|
+
error_handler.handle_cluster_health_check_failed_error(e, config)
|
|
576
|
+
sys.exit(1)
|
|
530
577
|
except FileNotFoundError as e:
|
|
531
|
-
|
|
578
|
+
error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
|
|
532
579
|
sys.exit(1)
|
|
533
580
|
except ValueError as e:
|
|
534
|
-
|
|
581
|
+
error_handler.handle_config_validation_error(
|
|
582
|
+
exceptions.ConfigValidationError(str(e)), config
|
|
583
|
+
)
|
|
535
584
|
sys.exit(1)
|
|
536
|
-
except
|
|
537
|
-
|
|
585
|
+
except exceptions.ConcurrencyConflictError as e:
|
|
586
|
+
error_handler.handle_concurrency_conflict_error(e, config)
|
|
538
587
|
sys.exit(1)
|
|
539
588
|
except Exception as e:
|
|
540
589
|
logger.error(f"Unexpected error: {e}")
|
starrocks_br/concurrency.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
|
|
3
|
-
from . import logger, utils
|
|
3
|
+
from . import exceptions, logger, utils
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def reserve_job_slot(db, scope: str, label: str) -> None:
|
|
@@ -46,14 +46,7 @@ def _can_heal_stale_job(scope: str, label: str, db) -> bool:
|
|
|
46
46
|
|
|
47
47
|
def _raise_concurrency_conflict(scope: str, active_jobs: list[tuple[str, str, str]]) -> None:
|
|
48
48
|
"""Raise a concurrency conflict error with helpful message."""
|
|
49
|
-
|
|
50
|
-
active_labels = [job[1] for job in active_jobs]
|
|
51
|
-
|
|
52
|
-
raise RuntimeError(
|
|
53
|
-
f"Concurrency conflict: Another '{scope}' job is already ACTIVE: {', '.join(active_job_strings)}. "
|
|
54
|
-
f"Wait for it to complete or cancel it via: UPDATE ops.run_status SET state='CANCELLED' "
|
|
55
|
-
f"WHERE label='{active_labels[0]}' AND state='ACTIVE'"
|
|
56
|
-
)
|
|
49
|
+
raise exceptions.ConcurrencyConflictError(scope, active_jobs)
|
|
57
50
|
|
|
58
51
|
|
|
59
52
|
def _insert_new_job(db, scope: str, label: str) -> None:
|
starrocks_br/config.py
CHANGED
|
@@ -2,6 +2,8 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
import yaml
|
|
4
4
|
|
|
5
|
+
from . import exceptions
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def load_config(config_path: str) -> dict[str, Any]:
|
|
7
9
|
"""Load and parse YAML configuration file.
|
|
@@ -20,7 +22,7 @@ def load_config(config_path: str) -> dict[str, Any]:
|
|
|
20
22
|
config = yaml.safe_load(f)
|
|
21
23
|
|
|
22
24
|
if not isinstance(config, dict):
|
|
23
|
-
raise
|
|
25
|
+
raise exceptions.ConfigValidationError("Config must be a dictionary")
|
|
24
26
|
|
|
25
27
|
return config
|
|
26
28
|
|
|
@@ -32,13 +34,13 @@ def validate_config(config: dict[str, Any]) -> None:
|
|
|
32
34
|
config: Configuration dictionary
|
|
33
35
|
|
|
34
36
|
Raises:
|
|
35
|
-
|
|
37
|
+
ConfigValidationError: If required fields are missing
|
|
36
38
|
"""
|
|
37
39
|
required_fields = ["host", "port", "user", "database", "repository"]
|
|
38
40
|
|
|
39
41
|
for field in required_fields:
|
|
40
42
|
if field not in config:
|
|
41
|
-
raise
|
|
43
|
+
raise exceptions.ConfigValidationError(f"Missing required config field: {field}")
|
|
42
44
|
|
|
43
45
|
_validate_tls_section(config.get("tls"))
|
|
44
46
|
|
|
@@ -48,17 +50,19 @@ def _validate_tls_section(tls_config) -> None:
|
|
|
48
50
|
return
|
|
49
51
|
|
|
50
52
|
if not isinstance(tls_config, dict):
|
|
51
|
-
raise
|
|
53
|
+
raise exceptions.ConfigValidationError("TLS configuration must be a dictionary")
|
|
52
54
|
|
|
53
55
|
enabled = bool(tls_config.get("enabled", False))
|
|
54
56
|
|
|
55
57
|
if enabled and not tls_config.get("ca_cert"):
|
|
56
|
-
raise
|
|
58
|
+
raise exceptions.ConfigValidationError(
|
|
59
|
+
"TLS configuration requires 'ca_cert' when 'enabled' is true"
|
|
60
|
+
)
|
|
57
61
|
|
|
58
62
|
if "verify_server_cert" in tls_config and not isinstance(
|
|
59
63
|
tls_config["verify_server_cert"], bool
|
|
60
64
|
):
|
|
61
|
-
raise
|
|
65
|
+
raise exceptions.ConfigValidationError(
|
|
62
66
|
"TLS configuration field 'verify_server_cert' must be a boolean if provided"
|
|
63
67
|
)
|
|
64
68
|
|
|
@@ -67,6 +71,6 @@ def _validate_tls_section(tls_config) -> None:
|
|
|
67
71
|
if not isinstance(tls_versions, list) or not all(
|
|
68
72
|
isinstance(version, str) for version in tls_versions
|
|
69
73
|
):
|
|
70
|
-
raise
|
|
74
|
+
raise exceptions.ConfigValidationError(
|
|
71
75
|
"TLS configuration field 'tls_versions' must be a list of strings if provided"
|
|
72
76
|
)
|
starrocks_br/db.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
3
|
import mysql.connector
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ class StarRocksDB:
|
|
|
13
13
|
user: str,
|
|
14
14
|
password: str,
|
|
15
15
|
database: str,
|
|
16
|
-
tls_config:
|
|
16
|
+
tls_config: dict[str, Any] | None = None,
|
|
17
17
|
):
|
|
18
18
|
"""Initialize database connection.
|
|
19
19
|
|
|
@@ -31,7 +31,7 @@ class StarRocksDB:
|
|
|
31
31
|
self.database = database
|
|
32
32
|
self._connection = None
|
|
33
33
|
self.tls_config = tls_config or {}
|
|
34
|
-
self._timezone:
|
|
34
|
+
self._timezone: str | None = None
|
|
35
35
|
|
|
36
36
|
def connect(self) -> None:
|
|
37
37
|
"""Establish database connection."""
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from . import exceptions
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def display_structured_error(
|
|
7
|
+
title: str,
|
|
8
|
+
reason: str,
|
|
9
|
+
what_to_do: list[str],
|
|
10
|
+
inputs: dict = None,
|
|
11
|
+
help_links: list[str] = None,
|
|
12
|
+
) -> None:
|
|
13
|
+
click.echo()
|
|
14
|
+
click.echo(click.style(f"❌ {title}", fg="red", bold=True), err=True)
|
|
15
|
+
click.echo(reason, err=True)
|
|
16
|
+
click.echo()
|
|
17
|
+
|
|
18
|
+
click.echo(click.style("REASON", fg="yellow", bold=True), err=True)
|
|
19
|
+
click.echo(reason, err=True)
|
|
20
|
+
click.echo()
|
|
21
|
+
|
|
22
|
+
click.echo(click.style("WHAT YOU CAN DO", fg="cyan", bold=True), err=True)
|
|
23
|
+
for i, action in enumerate(what_to_do, 1):
|
|
24
|
+
click.echo(f"{i}) {action}", err=True)
|
|
25
|
+
click.echo()
|
|
26
|
+
|
|
27
|
+
if inputs:
|
|
28
|
+
click.echo(click.style("INPUT YOU PROVIDED", fg="magenta", bold=True), err=True)
|
|
29
|
+
for key, value in inputs.items():
|
|
30
|
+
if value is None:
|
|
31
|
+
click.echo(f" Missing: {key}", err=True)
|
|
32
|
+
else:
|
|
33
|
+
click.echo(f" {key}: {value}", err=True)
|
|
34
|
+
click.echo()
|
|
35
|
+
|
|
36
|
+
if help_links:
|
|
37
|
+
click.echo(click.style("NEED HELP?", fg="green", bold=True), err=True)
|
|
38
|
+
for link in help_links:
|
|
39
|
+
click.echo(f" → {link}", err=True)
|
|
40
|
+
click.echo()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_missing_option_error(exc: exceptions.MissingOptionError, config: str = None) -> None:
|
|
44
|
+
display_structured_error(
|
|
45
|
+
title="OPERATION FAILED",
|
|
46
|
+
reason=f'The "{exc.missing_option}" option was not provided.\nThis parameter is required for the operation.',
|
|
47
|
+
what_to_do=[
|
|
48
|
+
f"Add the missing parameter: {exc.missing_option}",
|
|
49
|
+
"Run the command with --help to see all required options",
|
|
50
|
+
f"Example: starrocks-br <command> {exc.missing_option} <value>"
|
|
51
|
+
+ (f" --config {config}" if config else ""),
|
|
52
|
+
],
|
|
53
|
+
inputs={"--config": config, "Missing": exc.missing_option}
|
|
54
|
+
if config
|
|
55
|
+
else {"Missing": exc.missing_option},
|
|
56
|
+
help_links=["Run with --help for more information"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def handle_backup_label_not_found_error(
|
|
61
|
+
exc: exceptions.BackupLabelNotFoundError, config: str = None
|
|
62
|
+
) -> None:
|
|
63
|
+
display_structured_error(
|
|
64
|
+
title="RESTORE FAILED",
|
|
65
|
+
reason=f'The backup label "{exc.label}" does not exist in the repository'
|
|
66
|
+
+ (f' "{exc.repository}"' if exc.repository else "")
|
|
67
|
+
+ ",\nor the backup did not complete successfully.",
|
|
68
|
+
what_to_do=[
|
|
69
|
+
"List available backups by querying the backup history table:\n SELECT label, backup_type, status, finished_at FROM ops.backup_history ORDER BY finished_at DESC;",
|
|
70
|
+
"Check whether the backup completed successfully using StarRocks SQL:"
|
|
71
|
+
+ (
|
|
72
|
+
f"\n SHOW BACKUP FROM `{exc.repository}`;"
|
|
73
|
+
if exc.repository
|
|
74
|
+
else "\n SHOW BACKUP;"
|
|
75
|
+
),
|
|
76
|
+
"Verify that the backup label spelling is correct.",
|
|
77
|
+
],
|
|
78
|
+
inputs={"--config": config, "--target-label": exc.label, "Repository": exc.repository},
|
|
79
|
+
help_links=["starrocks-br restore --help"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def handle_no_successful_full_backup_found_error(
|
|
84
|
+
exc: exceptions.NoSuccessfulFullBackupFoundError, config: str = None
|
|
85
|
+
) -> None:
|
|
86
|
+
display_structured_error(
|
|
87
|
+
title="RESTORE FAILED",
|
|
88
|
+
reason=f'No successful full backup was found before the incremental backup "{exc.incremental_label}".\nIncremental backups require a base full backup to restore from.',
|
|
89
|
+
what_to_do=[
|
|
90
|
+
"Verify that a full backup was created before this incremental backup:\n SELECT label, backup_type, status, finished_at FROM ops.backup_history WHERE backup_type = 'full' AND status = 'FINISHED' ORDER BY finished_at DESC;",
|
|
91
|
+
"Run a full backup first:\n starrocks-br backup full --config "
|
|
92
|
+
+ (config if config else "<config.yaml>")
|
|
93
|
+
+ " --group <group_name>",
|
|
94
|
+
"Check that the full backup completed successfully before running the incremental backup",
|
|
95
|
+
],
|
|
96
|
+
inputs={"--target-label": exc.incremental_label, "--config": config},
|
|
97
|
+
help_links=["starrocks-br backup full --help"],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def handle_table_not_found_in_backup_error(
|
|
102
|
+
exc: exceptions.TableNotFoundInBackupError, config: str = None
|
|
103
|
+
) -> None:
|
|
104
|
+
display_structured_error(
|
|
105
|
+
title="TABLE NOT FOUND",
|
|
106
|
+
reason=f'Table "{exc.table}" was not found in backup "{exc.label}" for database "{exc.database}".',
|
|
107
|
+
what_to_do=[
|
|
108
|
+
"List all tables in the backup:"
|
|
109
|
+
+ (
|
|
110
|
+
f"\n SELECT DISTINCT database_name, table_name FROM ops.backup_partitions WHERE label = '{exc.label}';"
|
|
111
|
+
if config
|
|
112
|
+
else ""
|
|
113
|
+
),
|
|
114
|
+
"Verify the table name spelling is correct",
|
|
115
|
+
f"Ensure the table was included in the backup {exc.label}",
|
|
116
|
+
],
|
|
117
|
+
inputs={
|
|
118
|
+
"--table": exc.table,
|
|
119
|
+
"--target-label": exc.label,
|
|
120
|
+
"Database": exc.database,
|
|
121
|
+
"--config": config,
|
|
122
|
+
},
|
|
123
|
+
help_links=["starrocks-br restore --help"],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def handle_invalid_table_name_error(exc: exceptions.InvalidTableNameError) -> None:
|
|
128
|
+
display_structured_error(
|
|
129
|
+
title="INVALID TABLE NAME",
|
|
130
|
+
reason=f'The table name "{exc.table_name}" is invalid.\n{exc.reason}',
|
|
131
|
+
what_to_do=[
|
|
132
|
+
"Use only the table name without database prefix",
|
|
133
|
+
"Example: Use 'my_table' instead of 'database.my_table'",
|
|
134
|
+
"The database name should come from the config file",
|
|
135
|
+
],
|
|
136
|
+
inputs={"--table": exc.table_name},
|
|
137
|
+
help_links=["starrocks-br restore --help"],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def handle_config_file_not_found_error(exc: exceptions.ConfigFileNotFoundError) -> None:
|
|
142
|
+
display_structured_error(
|
|
143
|
+
title="CONFIG FILE NOT FOUND",
|
|
144
|
+
reason=f'The configuration file "{exc.config_path}" could not be found.',
|
|
145
|
+
what_to_do=[
|
|
146
|
+
"Verify the config file path is correct",
|
|
147
|
+
"Ensure the file exists at the specified location",
|
|
148
|
+
"Create a config file with the required settings:\n host, port, user, database, repository",
|
|
149
|
+
],
|
|
150
|
+
inputs={"--config": exc.config_path},
|
|
151
|
+
help_links=["Check the documentation for config file format"],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def handle_config_validation_error(
|
|
156
|
+
exc: exceptions.ConfigValidationError, config: str = None
|
|
157
|
+
) -> None:
|
|
158
|
+
display_structured_error(
|
|
159
|
+
title="CONFIGURATION ERROR",
|
|
160
|
+
reason=str(exc),
|
|
161
|
+
what_to_do=[
|
|
162
|
+
"Review your configuration file for missing or invalid settings",
|
|
163
|
+
"Ensure all required fields are present: host, port, user, database, repository",
|
|
164
|
+
"Check that values are in the correct format",
|
|
165
|
+
],
|
|
166
|
+
inputs={"--config": config},
|
|
167
|
+
help_links=["Check the documentation for config file requirements"],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def handle_cluster_health_check_failed_error(
|
|
172
|
+
exc: exceptions.ClusterHealthCheckFailedError, config: str = None
|
|
173
|
+
) -> None:
|
|
174
|
+
display_structured_error(
|
|
175
|
+
title="CLUSTER HEALTH CHECK FAILED",
|
|
176
|
+
reason=f"The StarRocks cluster health check failed: {exc.health_message}",
|
|
177
|
+
what_to_do=[
|
|
178
|
+
"Check that the StarRocks cluster is running",
|
|
179
|
+
"Verify database connectivity settings in your config file",
|
|
180
|
+
"Check cluster status with: SHOW PROC '/frontends'; SHOW PROC '/backends';",
|
|
181
|
+
],
|
|
182
|
+
inputs={"--config": config},
|
|
183
|
+
help_links=["Check StarRocks documentation for troubleshooting cluster issues"],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def handle_snapshot_not_found_error(
|
|
188
|
+
exc: exceptions.SnapshotNotFoundError, config: str = None
|
|
189
|
+
) -> None:
|
|
190
|
+
display_structured_error(
|
|
191
|
+
title="SNAPSHOT NOT FOUND",
|
|
192
|
+
reason=f'Snapshot "{exc.snapshot_name}" was not found in repository "{exc.repository}".',
|
|
193
|
+
what_to_do=[
|
|
194
|
+
f"List available snapshots:\n SHOW SNAPSHOT ON {exc.repository};",
|
|
195
|
+
"Verify the snapshot name spelling is correct",
|
|
196
|
+
"Ensure the backup completed successfully:\n SELECT * FROM ops.backup_history WHERE label = '"
|
|
197
|
+
+ exc.snapshot_name
|
|
198
|
+
+ "';",
|
|
199
|
+
],
|
|
200
|
+
inputs={"Snapshot": exc.snapshot_name, "Repository": exc.repository, "--config": config},
|
|
201
|
+
help_links=["starrocks-br restore --help"],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def handle_no_partitions_found_error(
|
|
206
|
+
exc: exceptions.NoPartitionsFoundError, config: str = None, group: str = None
|
|
207
|
+
) -> None:
|
|
208
|
+
display_structured_error(
|
|
209
|
+
title="NO PARTITIONS FOUND",
|
|
210
|
+
reason="No partitions were found to backup"
|
|
211
|
+
+ (f" for group '{exc.group_name}'" if exc.group_name else "")
|
|
212
|
+
+ ".",
|
|
213
|
+
what_to_do=[
|
|
214
|
+
"Verify that the inventory group exists in ops.table_inventory:\n SELECT * FROM ops.table_inventory WHERE inventory_group = "
|
|
215
|
+
+ (f"'{exc.group_name}';" if exc.group_name else "'<your_group>';"),
|
|
216
|
+
"Check that the tables in the group have partitions",
|
|
217
|
+
"Ensure the baseline backup date is correct",
|
|
218
|
+
],
|
|
219
|
+
inputs={"--group": exc.group_name or group, "--config": config},
|
|
220
|
+
help_links=["starrocks-br backup incremental --help"],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def handle_no_tables_found_error(
|
|
225
|
+
exc: exceptions.NoTablesFoundError, config: str = None, target_label: str = None
|
|
226
|
+
) -> None:
|
|
227
|
+
display_structured_error(
|
|
228
|
+
title="NO TABLES FOUND",
|
|
229
|
+
reason="No tables were found"
|
|
230
|
+
+ (
|
|
231
|
+
f" in backup '{exc.label}' for group '{exc.group}'"
|
|
232
|
+
if exc.group and exc.label
|
|
233
|
+
else f" in backup '{exc.label}'"
|
|
234
|
+
if exc.label
|
|
235
|
+
else ""
|
|
236
|
+
)
|
|
237
|
+
+ ".",
|
|
238
|
+
what_to_do=[
|
|
239
|
+
"Verify that tables exist in the backup manifest:\n SELECT DISTINCT database_name, table_name FROM ops.backup_partitions WHERE label = "
|
|
240
|
+
+ (f"'{exc.label}';" if exc.label else "'<label>';"),
|
|
241
|
+
"Check that the group name is correct in ops.table_inventory"
|
|
242
|
+
if exc.group
|
|
243
|
+
else "Verify the backup completed successfully",
|
|
244
|
+
"List available backups:\n SELECT label, backup_type, status, finished_at FROM ops.backup_history ORDER BY finished_at DESC;",
|
|
245
|
+
],
|
|
246
|
+
inputs={
|
|
247
|
+
"--target-label": exc.label or target_label,
|
|
248
|
+
"--group": exc.group,
|
|
249
|
+
"--config": config,
|
|
250
|
+
},
|
|
251
|
+
help_links=["starrocks-br restore --help"],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def handle_restore_operation_cancelled_error() -> None:
|
|
256
|
+
display_structured_error(
|
|
257
|
+
title="OPERATION CANCELLED",
|
|
258
|
+
reason="The restore operation was cancelled by the user.",
|
|
259
|
+
what_to_do=[
|
|
260
|
+
"Review the restore plan carefully",
|
|
261
|
+
"Run again and confirm with 'Y' to proceed",
|
|
262
|
+
"Use the --yes flag to skip the confirmation prompt:\n starrocks-br restore --yes ...",
|
|
263
|
+
],
|
|
264
|
+
help_links=["starrocks-br restore --help"],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def handle_concurrency_conflict_error(
|
|
269
|
+
exc: exceptions.ConcurrencyConflictError, config: str = None
|
|
270
|
+
) -> None:
|
|
271
|
+
active_job_strings = [f"{job[0]}:{job[1]}" for job in exc.active_jobs]
|
|
272
|
+
first_label = exc.active_labels[0] if exc.active_labels else "unknown"
|
|
273
|
+
|
|
274
|
+
display_structured_error(
|
|
275
|
+
title="CONCURRENCY CONFLICT",
|
|
276
|
+
reason=f"Another '{exc.scope}' job is already running.\nOnly one job of the same type can run at a time to prevent conflicts.",
|
|
277
|
+
what_to_do=[
|
|
278
|
+
f"Wait for the active job to complete: {', '.join(active_job_strings)}",
|
|
279
|
+
f"Check the job status in ops.run_status:\n SELECT * FROM ops.run_status WHERE label = '{first_label}' AND state = 'ACTIVE';",
|
|
280
|
+
f"If the job is stuck, cancel it manually:\n UPDATE ops.run_status SET state = 'CANCELLED' WHERE label = '{first_label}' AND state = 'ACTIVE';",
|
|
281
|
+
"Verify the job is not actually running in StarRocks before cancelling it",
|
|
282
|
+
],
|
|
283
|
+
inputs={
|
|
284
|
+
"--config": config,
|
|
285
|
+
"Scope": exc.scope,
|
|
286
|
+
"Active jobs": ", ".join(active_job_strings),
|
|
287
|
+
},
|
|
288
|
+
help_links=["Check ops.run_status table for job status"],
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def handle_no_full_backup_found_error(
|
|
293
|
+
exc: exceptions.NoFullBackupFoundError, config: str = None, group: str = None
|
|
294
|
+
) -> None:
|
|
295
|
+
display_structured_error(
|
|
296
|
+
title="NO FULL BACKUP FOUND",
|
|
297
|
+
reason=f"No successful full backup was found for database '{exc.database}'.\nIncremental backups require a baseline full backup to compare against.",
|
|
298
|
+
what_to_do=[
|
|
299
|
+
"Run a full backup first:\n starrocks-br backup full --config "
|
|
300
|
+
+ (config if config else "<config.yaml>")
|
|
301
|
+
+ f" --group {group if group else '<group_name>'}",
|
|
302
|
+
f"Verify no full backups exist for this database:\n SELECT label, backup_type, status, finished_at FROM ops.backup_history WHERE backup_type = 'full' AND label LIKE '{exc.database}_%' ORDER BY finished_at DESC;",
|
|
303
|
+
"After the full backup completes successfully, retry the incremental backup",
|
|
304
|
+
],
|
|
305
|
+
inputs={"Database": exc.database, "--config": config, "--group": group},
|
|
306
|
+
help_links=["starrocks-br backup full --help"],
|
|
307
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
class StarRocksBRError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MissingOptionError(StarRocksBRError):
|
|
6
|
+
def __init__(self, missing_option: str):
|
|
7
|
+
self.missing_option = missing_option
|
|
8
|
+
super().__init__(f"Missing required option: {missing_option}")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BackupLabelNotFoundError(StarRocksBRError):
|
|
12
|
+
def __init__(self, label: str, repository: str = None):
|
|
13
|
+
self.label = label
|
|
14
|
+
self.repository = repository
|
|
15
|
+
if repository:
|
|
16
|
+
super().__init__(f"Backup label '{label}' not found in repository '{repository}'")
|
|
17
|
+
else:
|
|
18
|
+
super().__init__(f"Backup label '{label}' not found")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NoSuccessfulFullBackupFoundError(StarRocksBRError):
|
|
22
|
+
def __init__(self, incremental_label: str):
|
|
23
|
+
self.incremental_label = incremental_label
|
|
24
|
+
super().__init__(
|
|
25
|
+
f"No successful full backup found before incremental '{incremental_label}'"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TableNotFoundInBackupError(StarRocksBRError):
|
|
30
|
+
def __init__(self, table: str, label: str, database: str):
|
|
31
|
+
self.table = table
|
|
32
|
+
self.label = label
|
|
33
|
+
self.database = database
|
|
34
|
+
super().__init__(f"Table '{table}' not found in backup '{label}' for database '{database}'")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InvalidTableNameError(StarRocksBRError):
|
|
38
|
+
def __init__(self, table_name: str, reason: str):
|
|
39
|
+
self.table_name = table_name
|
|
40
|
+
self.reason = reason
|
|
41
|
+
super().__init__(f"Invalid table name '{table_name}': {reason}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConfigFileNotFoundError(StarRocksBRError):
|
|
45
|
+
def __init__(self, config_path: str):
|
|
46
|
+
self.config_path = config_path
|
|
47
|
+
super().__init__(f"Config file not found: {config_path}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConfigValidationError(StarRocksBRError):
|
|
51
|
+
def __init__(self, message: str):
|
|
52
|
+
super().__init__(f"Configuration error: {message}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ClusterHealthCheckFailedError(StarRocksBRError):
|
|
56
|
+
def __init__(self, message: str):
|
|
57
|
+
self.health_message = message
|
|
58
|
+
super().__init__(f"Cluster health check failed: {message}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SnapshotNotFoundError(StarRocksBRError):
|
|
62
|
+
def __init__(self, snapshot_name: str, repository: str):
|
|
63
|
+
self.snapshot_name = snapshot_name
|
|
64
|
+
self.repository = repository
|
|
65
|
+
super().__init__(f"Snapshot '{snapshot_name}' not found in repository '{repository}'")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NoPartitionsFoundError(StarRocksBRError):
|
|
69
|
+
def __init__(self, group_name: str = None):
|
|
70
|
+
self.group_name = group_name
|
|
71
|
+
if group_name:
|
|
72
|
+
super().__init__(f"No partitions found to backup for group '{group_name}'")
|
|
73
|
+
else:
|
|
74
|
+
super().__init__("No partitions found to backup")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class NoTablesFoundError(StarRocksBRError):
|
|
78
|
+
def __init__(self, group: str = None, label: str = None):
|
|
79
|
+
self.group = group
|
|
80
|
+
self.label = label
|
|
81
|
+
if group and label:
|
|
82
|
+
super().__init__(f"No tables found in backup '{label}' for group '{group}'")
|
|
83
|
+
elif group:
|
|
84
|
+
super().__init__(f"No tables found for group '{group}'")
|
|
85
|
+
elif label:
|
|
86
|
+
super().__init__(f"No tables found in backup '{label}'")
|
|
87
|
+
else:
|
|
88
|
+
super().__init__("No tables found")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RestoreOperationCancelledError(StarRocksBRError):
|
|
92
|
+
def __init__(self):
|
|
93
|
+
super().__init__("Restore operation cancelled by user")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ConcurrencyConflictError(StarRocksBRError):
|
|
97
|
+
def __init__(self, scope: str, active_jobs: list[tuple[str, str, str]]):
|
|
98
|
+
self.scope = scope
|
|
99
|
+
self.active_jobs = active_jobs
|
|
100
|
+
self.active_labels = [job[1] for job in active_jobs]
|
|
101
|
+
super().__init__(
|
|
102
|
+
f"Concurrency conflict: Another '{scope}' job is already active: {', '.join(f'{job[0]}:{job[1]}' for job in active_jobs)}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NoFullBackupFoundError(StarRocksBRError):
|
|
107
|
+
def __init__(self, database: str):
|
|
108
|
+
self.database = database
|
|
109
|
+
super().__init__(f"No successful full backup found for database '{database}'")
|
starrocks_br/executor.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import time
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Literal
|
|
4
4
|
|
|
5
5
|
from . import concurrency, history, logger, timezone
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ def _calculate_next_interval(current_interval: float, max_interval: float) -> fl
|
|
|
22
22
|
|
|
23
23
|
def submit_backup_command(
|
|
24
24
|
db, backup_command: str
|
|
25
|
-
) -> tuple[bool,
|
|
25
|
+
) -> tuple[bool, str | None, dict[str, str] | None]:
|
|
26
26
|
"""Submit a backup command to StarRocks.
|
|
27
27
|
|
|
28
28
|
Returns (success, error_message, error_details).
|
|
@@ -50,7 +50,7 @@ def submit_backup_command(
|
|
|
50
50
|
return False, error_msg, None
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def _check_snapshot_exists_error(exception: Exception, error_str: str) ->
|
|
53
|
+
def _check_snapshot_exists_error(exception: Exception, error_str: str) -> str | None:
|
|
54
54
|
"""Check if the error is a 'snapshot already exists' error and extract snapshot name.
|
|
55
55
|
|
|
56
56
|
Args:
|
|
@@ -166,7 +166,7 @@ def execute_backup(
|
|
|
166
166
|
repository: str,
|
|
167
167
|
backup_type: Literal["incremental", "full"] = None,
|
|
168
168
|
scope: str = "backup",
|
|
169
|
-
database:
|
|
169
|
+
database: str | None = None,
|
|
170
170
|
) -> dict:
|
|
171
171
|
"""Execute a complete backup workflow: submit command and monitor progress.
|
|
172
172
|
|
starrocks_br/history.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
from . import logger
|
|
4
2
|
|
|
5
3
|
|
|
6
|
-
def log_backup(db, entry: dict[str,
|
|
4
|
+
def log_backup(db, entry: dict[str, str | None]) -> None:
|
|
7
5
|
"""Write a backup history entry to ops.backup_history.
|
|
8
6
|
|
|
9
7
|
Expected keys in entry:
|
|
@@ -24,7 +22,7 @@ def log_backup(db, entry: dict[str, Optional[str]]) -> None:
|
|
|
24
22
|
finished_at = entry.get("finished_at", "NULL")
|
|
25
23
|
error_message = entry.get("error_message")
|
|
26
24
|
|
|
27
|
-
def esc(val:
|
|
25
|
+
def esc(val: str | None) -> str:
|
|
28
26
|
if val is None:
|
|
29
27
|
return "NULL"
|
|
30
28
|
return "'" + str(val).replace("'", "''") + "'"
|
|
@@ -45,7 +43,7 @@ def log_backup(db, entry: dict[str, Optional[str]]) -> None:
|
|
|
45
43
|
raise
|
|
46
44
|
|
|
47
45
|
|
|
48
|
-
def log_restore(db, entry: dict[str,
|
|
46
|
+
def log_restore(db, entry: dict[str, str | None]) -> None:
|
|
49
47
|
"""Write a restore history entry to ops.restore_history.
|
|
50
48
|
|
|
51
49
|
Expected keys in entry:
|
|
@@ -69,7 +67,7 @@ def log_restore(db, entry: dict[str, Optional[str]]) -> None:
|
|
|
69
67
|
error_message = entry.get("error_message")
|
|
70
68
|
verification_checksum = entry.get("verification_checksum")
|
|
71
69
|
|
|
72
|
-
def esc(val:
|
|
70
|
+
def esc(val: str | None) -> str:
|
|
73
71
|
if val is None:
|
|
74
72
|
return "NULL"
|
|
75
73
|
return "'" + str(val).replace("'", "''") + "'"
|
starrocks_br/labels.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import Literal
|
|
2
|
+
from typing import Literal
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def determine_backup_label(
|
|
6
6
|
db,
|
|
7
7
|
backup_type: Literal["incremental", "full"],
|
|
8
8
|
database_name: str,
|
|
9
|
-
custom_name:
|
|
9
|
+
custom_name: str | None = None,
|
|
10
10
|
) -> str:
|
|
11
11
|
"""Determine a unique backup label for the given parameters.
|
|
12
12
|
|
starrocks_br/logger.py
CHANGED
|
@@ -1,36 +1,66 @@
|
|
|
1
|
-
import
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
_logger = None
|
|
5
|
+
_logger_lock = threading.Lock()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_logging(level: int = logging.INFO) -> None:
|
|
9
|
+
global _logger
|
|
10
|
+
_logger = logging.getLogger("starrocks_br")
|
|
11
|
+
_logger.setLevel(level)
|
|
12
|
+
|
|
13
|
+
if _logger.handlers:
|
|
14
|
+
_logger.handlers.clear()
|
|
15
|
+
|
|
16
|
+
handler = logging.StreamHandler()
|
|
17
|
+
|
|
18
|
+
if level == logging.DEBUG:
|
|
19
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
|
|
20
|
+
else:
|
|
21
|
+
formatter = logging.Formatter("%(message)s")
|
|
22
|
+
|
|
23
|
+
handler.setFormatter(formatter)
|
|
24
|
+
_logger.addHandler(handler)
|
|
25
|
+
_logger.propagate = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_logger() -> logging.Logger:
|
|
29
|
+
global _logger
|
|
30
|
+
if _logger is None:
|
|
31
|
+
with _logger_lock:
|
|
32
|
+
if _logger is None:
|
|
33
|
+
setup_logging()
|
|
34
|
+
return _logger
|
|
2
35
|
|
|
3
36
|
|
|
4
37
|
def info(message: str) -> None:
|
|
5
|
-
|
|
6
|
-
click.echo(message)
|
|
38
|
+
_get_logger().info(message)
|
|
7
39
|
|
|
8
40
|
|
|
9
41
|
def success(message: str) -> None:
|
|
10
|
-
"
|
|
11
|
-
click.echo(f"✓ {message}")
|
|
42
|
+
_get_logger().info(f"✓ {message}")
|
|
12
43
|
|
|
13
44
|
|
|
14
45
|
def warning(message: str) -> None:
|
|
15
|
-
"
|
|
16
|
-
click.echo(f"⚠ {message}", err=True)
|
|
46
|
+
_get_logger().warning(f"⚠ {message}")
|
|
17
47
|
|
|
18
48
|
|
|
19
49
|
def error(message: str) -> None:
|
|
20
|
-
"
|
|
21
|
-
click.echo(f"Error: {message}", err=True)
|
|
50
|
+
_get_logger().error(f"Error: {message}")
|
|
22
51
|
|
|
23
52
|
|
|
24
53
|
def critical(message: str) -> None:
|
|
25
|
-
"
|
|
26
|
-
click.echo(f"❌ CRITICAL: {message}", err=True)
|
|
54
|
+
_get_logger().critical(f"❌ CRITICAL: {message}")
|
|
27
55
|
|
|
28
56
|
|
|
29
57
|
def progress(message: str) -> None:
|
|
30
|
-
"
|
|
31
|
-
click.echo(f"⏳ {message}")
|
|
58
|
+
_get_logger().info(f"⏳ {message}")
|
|
32
59
|
|
|
33
60
|
|
|
34
61
|
def tip(message: str) -> None:
|
|
35
|
-
"
|
|
36
|
-
|
|
62
|
+
_get_logger().warning(f"💡 {message}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def debug(message: str) -> None:
|
|
66
|
+
_get_logger().debug(message)
|
starrocks_br/planner.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import hashlib
|
|
3
|
-
from typing import Optional
|
|
4
3
|
|
|
5
|
-
from starrocks_br import logger, timezone, utils
|
|
4
|
+
from starrocks_br import exceptions, logger, timezone, utils
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
def find_latest_full_backup(db, database: str) ->
|
|
7
|
+
def find_latest_full_backup(db, database: str) -> dict[str, str] | None:
|
|
9
8
|
"""Find the latest successful full backup for a database.
|
|
10
9
|
|
|
11
10
|
Args:
|
|
@@ -60,7 +59,7 @@ def find_tables_by_group(db, group_name: str) -> list[dict[str, str]]:
|
|
|
60
59
|
|
|
61
60
|
|
|
62
61
|
def find_recent_partitions(
|
|
63
|
-
db, database: str, baseline_backup_label:
|
|
62
|
+
db, database: str, baseline_backup_label: str | None = None, *, group_name: str
|
|
64
63
|
) -> list[dict[str, str]]:
|
|
65
64
|
"""Find partitions updated since baseline for tables in the given inventory group.
|
|
66
65
|
|
|
@@ -84,16 +83,12 @@ def find_recent_partitions(
|
|
|
84
83
|
"""
|
|
85
84
|
baseline_rows = db.query(baseline_query)
|
|
86
85
|
if not baseline_rows:
|
|
87
|
-
raise
|
|
88
|
-
f"Baseline backup '{baseline_backup_label}' not found or not successful"
|
|
89
|
-
)
|
|
86
|
+
raise exceptions.BackupLabelNotFoundError(baseline_backup_label)
|
|
90
87
|
baseline_time_raw = baseline_rows[0][0]
|
|
91
88
|
else:
|
|
92
89
|
latest_backup = find_latest_full_backup(db, database)
|
|
93
90
|
if not latest_backup:
|
|
94
|
-
raise
|
|
95
|
-
f"No successful full backup found for database '{database}'. Run a full database backup first."
|
|
96
|
-
)
|
|
91
|
+
raise exceptions.NoFullBackupFoundError(database)
|
|
97
92
|
baseline_time_raw = latest_backup["finished_at"]
|
|
98
93
|
|
|
99
94
|
if isinstance(baseline_time_raw, datetime.datetime):
|
starrocks_br/restore.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import time
|
|
3
|
-
from typing import Optional
|
|
4
3
|
|
|
5
|
-
from . import concurrency, history, logger, timezone, utils
|
|
4
|
+
from . import concurrency, exceptions, history, logger, timezone, utils
|
|
6
5
|
|
|
7
6
|
MAX_POLLS = 86400 # 1 day
|
|
8
7
|
|
|
@@ -38,7 +37,7 @@ def get_snapshot_timestamp(db, repo_name: str, snapshot_name: str) -> str:
|
|
|
38
37
|
|
|
39
38
|
rows = db.query(query)
|
|
40
39
|
if not rows:
|
|
41
|
-
raise
|
|
40
|
+
raise exceptions.SnapshotNotFoundError(snapshot_name, repo_name)
|
|
42
41
|
|
|
43
42
|
# The result should be a single row with columns: Snapshot, Timestamp, Status
|
|
44
43
|
result = rows[0]
|
|
@@ -274,7 +273,7 @@ def find_restore_pair(db, target_label: str) -> list[str]:
|
|
|
274
273
|
|
|
275
274
|
rows = db.query(query)
|
|
276
275
|
if not rows:
|
|
277
|
-
raise
|
|
276
|
+
raise exceptions.BackupLabelNotFoundError(target_label)
|
|
278
277
|
|
|
279
278
|
target_info = {"label": rows[0][0], "backup_type": rows[0][1], "finished_at": rows[0][2]}
|
|
280
279
|
|
|
@@ -297,7 +296,7 @@ def find_restore_pair(db, target_label: str) -> list[str]:
|
|
|
297
296
|
|
|
298
297
|
full_rows = db.query(full_backup_query)
|
|
299
298
|
if not full_rows:
|
|
300
|
-
raise
|
|
299
|
+
raise exceptions.NoSuccessfulFullBackupFoundError(target_label)
|
|
301
300
|
|
|
302
301
|
base_full_backup = full_rows[0][0]
|
|
303
302
|
return [base_full_backup, target_label]
|
|
@@ -310,9 +309,9 @@ def find_restore_pair(db, target_label: str) -> list[str]:
|
|
|
310
309
|
def get_tables_from_backup(
|
|
311
310
|
db,
|
|
312
311
|
label: str,
|
|
313
|
-
group:
|
|
314
|
-
table:
|
|
315
|
-
database:
|
|
312
|
+
group: str | None = None,
|
|
313
|
+
table: str | None = None,
|
|
314
|
+
database: str | None = None,
|
|
316
315
|
) -> list[str]:
|
|
317
316
|
"""Get list of tables to restore from backup manifest.
|
|
318
317
|
|
|
@@ -332,12 +331,12 @@ def get_tables_from_backup(
|
|
|
332
331
|
ValueError: If table is specified but not found in backup
|
|
333
332
|
"""
|
|
334
333
|
if group and table:
|
|
335
|
-
raise
|
|
336
|
-
"Cannot specify both --group and --table. Use --table for single table restore or --group for inventory group restore."
|
|
337
|
-
)
|
|
334
|
+
raise exceptions.InvalidTableNameError(table, "Cannot specify both --group and --table")
|
|
338
335
|
|
|
339
336
|
if table and not database:
|
|
340
|
-
raise
|
|
337
|
+
raise exceptions.InvalidTableNameError(
|
|
338
|
+
table, "database parameter is required when table is specified"
|
|
339
|
+
)
|
|
341
340
|
|
|
342
341
|
query = f"""
|
|
343
342
|
SELECT DISTINCT database_name, table_name
|
|
@@ -357,9 +356,7 @@ def get_tables_from_backup(
|
|
|
357
356
|
filtered_tables = [t for t in tables if t == target_table]
|
|
358
357
|
|
|
359
358
|
if not filtered_tables:
|
|
360
|
-
raise
|
|
361
|
-
f"Table '{table}' not found in backup '{label}' for database '{database}'"
|
|
362
|
-
)
|
|
359
|
+
raise exceptions.TableNotFoundInBackupError(table, label, database)
|
|
363
360
|
|
|
364
361
|
return filtered_tables
|
|
365
362
|
|
|
@@ -433,7 +430,7 @@ def execute_restore_flow(
|
|
|
433
430
|
if not skip_confirmation:
|
|
434
431
|
confirmation = input("\nDo you want to proceed? [Y/n]: ").strip()
|
|
435
432
|
if confirmation.lower() != "y":
|
|
436
|
-
|
|
433
|
+
raise exceptions.RestoreOperationCancelledError()
|
|
437
434
|
else:
|
|
438
435
|
logger.info("Proceeding automatically (--yes flag provided)")
|
|
439
436
|
|
starrocks_br/timezone.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
from typing import Union
|
|
3
2
|
from zoneinfo import ZoneInfo
|
|
4
3
|
|
|
5
4
|
|
|
@@ -55,7 +54,7 @@ def normalize_datetime_to_tz(dt: datetime.datetime, target_tz: str) -> datetime.
|
|
|
55
54
|
return dt
|
|
56
55
|
|
|
57
56
|
|
|
58
|
-
def _get_timezone(tz_str: str) ->
|
|
57
|
+
def _get_timezone(tz_str: str) -> ZoneInfo | datetime.timezone:
|
|
59
58
|
"""Get timezone object from timezone string.
|
|
60
59
|
|
|
61
60
|
Handles both named timezones (e.g., 'Asia/Shanghai') and offset strings (e.g., '+08:00', '-05:00').
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: starrocks-br
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: StarRocks Backup and Restore automation tool
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: click<9,>=8.1.7
|
|
8
8
|
Requires-Dist: PyYAML<7,>=6.0.1
|
|
@@ -12,6 +12,7 @@ Requires-Dist: pytest<9,>=8.3.2; extra == "dev"
|
|
|
12
12
|
Requires-Dist: pytest-mock<4,>=3.14.0; extra == "dev"
|
|
13
13
|
Requires-Dist: pytest-cov<6,>=5.0.0; extra == "dev"
|
|
14
14
|
Requires-Dist: ruff<1,>=0.8.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pre-commit<5,>=4.0.0; extra == "dev"
|
|
15
16
|
|
|
16
17
|
# StarRocks Backup & Restore
|
|
17
18
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
starrocks_br/__init__.py,sha256=i1m0FIl2IAXaVyNoya0ZNAx3WfhIp9I6VLhTz06qNFY,28
|
|
2
|
+
starrocks_br/cli.py,sha256=rTqKP4gSkMhX4rgAOhvDFZfwJQB7USRV9uYrDvJ-58c,22924
|
|
3
|
+
starrocks_br/concurrency.py,sha256=uj5s5wR5ixa3XIdrwLiy7ootc71mMg3F1Uw-t0w16Ik,5558
|
|
4
|
+
starrocks_br/config.py,sha256=VrNXy_YxqiRJ3vDUGzRStZFM5ttK-q5EN_JdNiDiNJ0,2250
|
|
5
|
+
starrocks_br/db.py,sha256=47ynDQ9kdykJRj_nrHxX020b9njozzQxiZBI9lFdS7A,4946
|
|
6
|
+
starrocks_br/error_handler.py,sha256=j9UW4vh7PaQW5tSi1YP-SqGuD420cCWo2-eFrkewCoU,13085
|
|
7
|
+
starrocks_br/exceptions.py,sha256=0KFwLKrNS4UGGvhc-54HncL6BX-wc6XnNVBvl8nYEus,3909
|
|
8
|
+
starrocks_br/executor.py,sha256=YE12jiU-4tru2D7BAe8Y0Fom72LHjGz04obN4FcAWhA,11345
|
|
9
|
+
starrocks_br/health.py,sha256=rmkgNYf6kk3VDZx-PmnAG3lzmtvnJcUPG7Ppb6BA7IU,1021
|
|
10
|
+
starrocks_br/history.py,sha256=ewXMVUHJvpWjvPndYUdz9xPh24HDPiUAuJgIALuWays,2964
|
|
11
|
+
starrocks_br/labels.py,sha256=07UFd8BMyyV2MQwf7NaLviuu37lMLOOFX3DCbf_XqOE,1662
|
|
12
|
+
starrocks_br/logger.py,sha256=8F7ZnqCOVFJDt6-rZevh94udGbhZhDLrBw8W3RZbM-4,1432
|
|
13
|
+
starrocks_br/planner.py,sha256=qk-T6SZmNzztrXwyS1jjwL1kSS_l6MRo3TNx-U-ECRw,10754
|
|
14
|
+
starrocks_br/repository.py,sha256=gZgT0mAjs-AAdESXPF8Syv0bE8m5njya5leTageElQ8,1251
|
|
15
|
+
starrocks_br/restore.py,sha256=7_VcrGt0KVqhWe9f3JgQYDMNuM6_EjBqCi63BiSa2WY,20105
|
|
16
|
+
starrocks_br/schema.py,sha256=FSJjcz4q3SU_rHLptsSzrlm-o0dcvIu6LbpT-Z5GyZA,6199
|
|
17
|
+
starrocks_br/timezone.py,sha256=WlB_gkgI4AjQzqHVA1eG9CY_9QiX5cYpVKjQLvSrd4Y,3578
|
|
18
|
+
starrocks_br/utils.py,sha256=LF3uBdaNMeslE4UHl_wwv4QErCS48ISxpPqYX8dbrc8,2176
|
|
19
|
+
starrocks_br-0.5.1.dist-info/METADATA,sha256=0n2LiJNNG8p_Es5zV8JRQhdSTcV8fkE2eCiwiSYTRas,5728
|
|
20
|
+
starrocks_br-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
starrocks_br-0.5.1.dist-info/entry_points.txt,sha256=AKUt01G2MAlh85s1Q9kNQDOUio14kaTnT3dmg9gjdNg,54
|
|
22
|
+
starrocks_br-0.5.1.dist-info/top_level.txt,sha256=CU1tGVo0kjulhDr761Sndg-oTeRKsisDnWm8UG95aBE,13
|
|
23
|
+
starrocks_br-0.5.1.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
starrocks_br/__init__.py,sha256=i1m0FIl2IAXaVyNoya0ZNAx3WfhIp9I6VLhTz06qNFY,28
|
|
2
|
-
starrocks_br/cli.py,sha256=yXrmfLL-doIZ9ahFKfXqGR09NB08jqz4BUhkkSCpEJc,20483
|
|
3
|
-
starrocks_br/concurrency.py,sha256=N0LD4VHTAFNhD4YslrkOCDSx5cnR5rCEkNH9MkODxv8,5903
|
|
4
|
-
starrocks_br/config.py,sha256=APqOZcJuUzYmGNHoJRlsu4l3sWl_4SS1kRLKjKm2Oag,2059
|
|
5
|
-
starrocks_br/db.py,sha256=gPFP_VcGmAM5uMADSFR9pNEbYN9IXDnN0O2YFMptkmM,4962
|
|
6
|
-
starrocks_br/executor.py,sha256=Z8k5oW-TRE0zg8on9_YtKqhT82Nq_ZmuGxC09Wx7M60,11367
|
|
7
|
-
starrocks_br/health.py,sha256=rmkgNYf6kk3VDZx-PmnAG3lzmtvnJcUPG7Ppb6BA7IU,1021
|
|
8
|
-
starrocks_br/history.py,sha256=kx2H0wGVYCoIvC1_zN4BMGv9d2YebR5oX3NlUd9yeWg,3005
|
|
9
|
-
starrocks_br/labels.py,sha256=-JfO34gNjYEGjSRW5haRRYYWvMrXRpNh9mTKXthAxzI,1675
|
|
10
|
-
starrocks_br/logger.py,sha256=QTfr-nC3TdeU7f1gcRTRDAQSLYpwaevd_iT1B_RbuF8,900
|
|
11
|
-
starrocks_br/planner.py,sha256=QcgBw_psqzWS2FnsmXvYHFgiOMMkfyEvMJryDUFhkpk,10926
|
|
12
|
-
starrocks_br/repository.py,sha256=gZgT0mAjs-AAdESXPF8Syv0bE8m5njya5leTageElQ8,1251
|
|
13
|
-
starrocks_br/restore.py,sha256=4XBcPMeplrqKyaU_yU5GhunAtPWtmvErOi2xOQPWQKA,20306
|
|
14
|
-
starrocks_br/schema.py,sha256=FSJjcz4q3SU_rHLptsSzrlm-o0dcvIu6LbpT-Z5GyZA,6199
|
|
15
|
-
starrocks_br/timezone.py,sha256=RYmM9RIEy1hphVGhwv7A8j1DyV9Djf-Y5HE8NxZMDtg,3609
|
|
16
|
-
starrocks_br/utils.py,sha256=LF3uBdaNMeslE4UHl_wwv4QErCS48ISxpPqYX8dbrc8,2176
|
|
17
|
-
starrocks_br-0.4.0.dist-info/METADATA,sha256=wYmVX_4K_i5K42fxA075GEUqCBSAbhfbAJ-qnvPx8I8,5675
|
|
18
|
-
starrocks_br-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
-
starrocks_br-0.4.0.dist-info/entry_points.txt,sha256=AKUt01G2MAlh85s1Q9kNQDOUio14kaTnT3dmg9gjdNg,54
|
|
20
|
-
starrocks_br-0.4.0.dist-info/top_level.txt,sha256=CU1tGVo0kjulhDr761Sndg-oTeRKsisDnWm8UG95aBE,13
|
|
21
|
-
starrocks_br-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|