starrocks-br 0.5.2__tar.gz → 0.6.0__tar.gz
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-0.5.2 → starrocks_br-0.6.0}/PKG-INFO +15 -2
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/README.md +14 -1
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/pyproject.toml +1 -1
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/cli.py +103 -36
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/concurrency.py +33 -23
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/config.py +75 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/error_handler.py +59 -12
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/exceptions.py +14 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/executor.py +9 -2
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/history.py +9 -9
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/labels.py +5 -3
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/planner.py +56 -13
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/repository.py +1 -1
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/restore.py +197 -40
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/schema.py +89 -43
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br.egg-info/PKG-INFO +15 -2
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_cli_backup.py +1 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_cli_exceptions.py +8 -3
- starrocks_br-0.6.0/tests/test_cli_init.py +80 -0
- starrocks_br-0.6.0/tests/test_config.py +405 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_error_handler.py +162 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_executor.py +1 -1
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_planner.py +107 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_restore.py +223 -1
- starrocks_br-0.6.0/tests/test_schema_setup.py +498 -0
- starrocks_br-0.5.2/tests/test_cli_init.py +0 -30
- starrocks_br-0.5.2/tests/test_config.py +0 -190
- starrocks_br-0.5.2/tests/test_schema_setup.py +0 -234
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/LICENSE +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/setup.cfg +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/__init__.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/db.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/health.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/logger.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/timezone.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br/utils.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br.egg-info/SOURCES.txt +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br.egg-info/dependency_links.txt +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br.egg-info/entry_points.txt +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br.egg-info/requires.txt +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/src/starrocks_br.egg-info/top_level.txt +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_cli_general.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_cli_restore.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_concurrency.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_db.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_health_checks.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_history.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_labels.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_logger.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_repository_sql.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_timezone.py +0 -0
- {starrocks_br-0.5.2 → starrocks_br-0.6.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: starrocks-br
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: StarRocks Backup and Restore automation tool
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -95,6 +95,15 @@ port: 9030 # MySQL protocol port
|
|
|
95
95
|
user: "root" # Database user with backup/restore privileges
|
|
96
96
|
database: "your_database" # Database containing tables to backup
|
|
97
97
|
repository: "your_repo_name" # Repository created via CREATE REPOSITORY in StarRocks
|
|
98
|
+
|
|
99
|
+
# Optional: Define table inventory groups directly in config
|
|
100
|
+
table_inventory:
|
|
101
|
+
- group: "production"
|
|
102
|
+
tables:
|
|
103
|
+
- database: "mydb"
|
|
104
|
+
table: "users"
|
|
105
|
+
- database: "mydb"
|
|
106
|
+
table: "orders"
|
|
98
107
|
```
|
|
99
108
|
|
|
100
109
|
Set password:
|
|
@@ -111,7 +120,11 @@ See [Configuration Reference](docs/configuration.md) for TLS and advanced option
|
|
|
111
120
|
starrocks-br init --config config.yaml
|
|
112
121
|
```
|
|
113
122
|
|
|
114
|
-
|
|
123
|
+
This creates the `ops` database and automatically populates table inventory from your config (if defined).
|
|
124
|
+
|
|
125
|
+
**Note:** If you modify the `table_inventory` in your config file, rerun `starrocks-br init --config config.yaml` to update the database.
|
|
126
|
+
|
|
127
|
+
**Alternative: Define inventory groups manually** (in StarRocks):
|
|
115
128
|
```sql
|
|
116
129
|
INSERT INTO ops.table_inventory (inventory_group, database_name, table_name)
|
|
117
130
|
VALUES
|
|
@@ -77,6 +77,15 @@ port: 9030 # MySQL protocol port
|
|
|
77
77
|
user: "root" # Database user with backup/restore privileges
|
|
78
78
|
database: "your_database" # Database containing tables to backup
|
|
79
79
|
repository: "your_repo_name" # Repository created via CREATE REPOSITORY in StarRocks
|
|
80
|
+
|
|
81
|
+
# Optional: Define table inventory groups directly in config
|
|
82
|
+
table_inventory:
|
|
83
|
+
- group: "production"
|
|
84
|
+
tables:
|
|
85
|
+
- database: "mydb"
|
|
86
|
+
table: "users"
|
|
87
|
+
- database: "mydb"
|
|
88
|
+
table: "orders"
|
|
80
89
|
```
|
|
81
90
|
|
|
82
91
|
Set password:
|
|
@@ -93,7 +102,11 @@ See [Configuration Reference](docs/configuration.md) for TLS and advanced option
|
|
|
93
102
|
starrocks-br init --config config.yaml
|
|
94
103
|
```
|
|
95
104
|
|
|
96
|
-
|
|
105
|
+
This creates the `ops` database and automatically populates table inventory from your config (if defined).
|
|
106
|
+
|
|
107
|
+
**Note:** If you modify the `table_inventory` in your config file, rerun `starrocks-br init --config config.yaml` to update the database.
|
|
108
|
+
|
|
109
|
+
**Alternative: Define inventory groups manually** (in StarRocks):
|
|
97
110
|
```sql
|
|
98
111
|
INSERT INTO ops.table_inventory (inventory_group, database_name, table_name)
|
|
99
112
|
VALUES
|
|
@@ -98,13 +98,13 @@ def cli(ctx, verbose):
|
|
|
98
98
|
@cli.command("init")
|
|
99
99
|
@click.option("--config", required=True, help="Path to config YAML file")
|
|
100
100
|
def init(config):
|
|
101
|
-
"""Initialize
|
|
101
|
+
"""Initialize operations database and control tables.
|
|
102
102
|
|
|
103
|
-
Creates the
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
103
|
+
Creates the operations database (default: 'ops') with required tables:
|
|
104
|
+
- table_inventory: Inventory groups mapping to databases/tables
|
|
105
|
+
- backup_history: Backup operation history
|
|
106
|
+
- restore_history: Restore operation history
|
|
107
|
+
- run_status: Job concurrency control
|
|
108
108
|
|
|
109
109
|
Run this once before using backup/restore commands.
|
|
110
110
|
"""
|
|
@@ -112,6 +112,9 @@ def init(config):
|
|
|
112
112
|
cfg = config_module.load_config(config)
|
|
113
113
|
config_module.validate_config(cfg)
|
|
114
114
|
|
|
115
|
+
ops_database = config_module.get_ops_database(cfg)
|
|
116
|
+
table_inventory_entries = config_module.get_table_inventory_entries(cfg)
|
|
117
|
+
|
|
115
118
|
database = db.StarRocksDB(
|
|
116
119
|
host=cfg["host"],
|
|
117
120
|
port=cfg["port"],
|
|
@@ -121,23 +124,43 @@ def init(config):
|
|
|
121
124
|
tls_config=cfg.get("tls"),
|
|
122
125
|
)
|
|
123
126
|
|
|
127
|
+
ops_database = config_module.get_ops_database(cfg)
|
|
128
|
+
|
|
124
129
|
with database:
|
|
125
|
-
logger.info("
|
|
126
|
-
|
|
127
|
-
logger.info("")
|
|
128
|
-
logger.info("Next steps:")
|
|
129
|
-
logger.info("1. Insert your table inventory records:")
|
|
130
|
-
logger.info(" INSERT INTO ops.table_inventory")
|
|
131
|
-
logger.info(" (inventory_group, database_name, table_name)")
|
|
132
|
-
logger.info(" VALUES ('my_daily_incremental', 'your_db', 'your_fact_table');")
|
|
133
|
-
logger.info(" VALUES ('my_full_database_backup', 'your_db', '*');")
|
|
134
|
-
logger.info(" VALUES ('my_full_dimension_tables', 'your_db', 'dim_customers');")
|
|
135
|
-
logger.info(" VALUES ('my_full_dimension_tables', 'your_db', 'dim_products');")
|
|
130
|
+
logger.info("Validating repository...")
|
|
131
|
+
repository.ensure_repository(database, cfg["repository"])
|
|
136
132
|
logger.info("")
|
|
137
|
-
|
|
138
|
-
logger.info(
|
|
139
|
-
|
|
133
|
+
|
|
134
|
+
logger.info("Initializing ops schema...")
|
|
135
|
+
schema.initialize_ops_schema(
|
|
136
|
+
database, ops_database=ops_database, table_inventory_entries=table_inventory_entries
|
|
140
137
|
)
|
|
138
|
+
logger.info("")
|
|
139
|
+
|
|
140
|
+
if table_inventory_entries:
|
|
141
|
+
logger.success(
|
|
142
|
+
f"Table inventory bootstrapped from config with {len(table_inventory_entries)} entries"
|
|
143
|
+
)
|
|
144
|
+
logger.info("")
|
|
145
|
+
logger.info("Next steps:")
|
|
146
|
+
logger.info("1. Run your first backup:")
|
|
147
|
+
logger.info(
|
|
148
|
+
f" starrocks-br backup incremental --group <your_group_name> --config {config}"
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
logger.info("Next steps:")
|
|
152
|
+
logger.info("1. Insert your table inventory records:")
|
|
153
|
+
logger.info(f" INSERT INTO {ops_database}.table_inventory")
|
|
154
|
+
logger.info(" (inventory_group, database_name, table_name)")
|
|
155
|
+
logger.info(" VALUES ('my_daily_incremental', 'your_db', 'your_fact_table');")
|
|
156
|
+
logger.info(" VALUES ('my_full_database_backup', 'your_db', '*');")
|
|
157
|
+
logger.info(" VALUES ('my_full_dimension_tables', 'your_db', 'dim_customers');")
|
|
158
|
+
logger.info(" VALUES ('my_full_dimension_tables', 'your_db', 'dim_products');")
|
|
159
|
+
logger.info("")
|
|
160
|
+
logger.info("2. Run your first backup:")
|
|
161
|
+
logger.info(
|
|
162
|
+
" starrocks-br backup incremental --group my_daily_incremental --config config.yaml"
|
|
163
|
+
)
|
|
141
164
|
|
|
142
165
|
except exceptions.ConfigFileNotFoundError as e:
|
|
143
166
|
error_handler.handle_config_file_not_found_error(e)
|
|
@@ -196,13 +219,17 @@ def backup_incremental(config, baseline_backup, group, name):
|
|
|
196
219
|
tls_config=cfg.get("tls"),
|
|
197
220
|
)
|
|
198
221
|
|
|
222
|
+
ops_database = config_module.get_ops_database(cfg)
|
|
223
|
+
|
|
199
224
|
with database:
|
|
200
|
-
was_created = schema.ensure_ops_schema(database)
|
|
225
|
+
was_created = schema.ensure_ops_schema(database, ops_database=ops_database)
|
|
201
226
|
if was_created:
|
|
202
227
|
logger.warning(
|
|
203
228
|
"ops schema was auto-created. Please run 'starrocks-br init' after populating config."
|
|
204
229
|
)
|
|
205
|
-
logger.warning(
|
|
230
|
+
logger.warning(
|
|
231
|
+
"Remember to populate the table_inventory table with your backup groups!"
|
|
232
|
+
)
|
|
206
233
|
sys.exit(1) # Exit if schema was just created, requires user action
|
|
207
234
|
|
|
208
235
|
healthy, message = health.check_cluster_health(database)
|
|
@@ -221,6 +248,7 @@ def backup_incremental(config, baseline_backup, group, name):
|
|
|
221
248
|
backup_type="incremental",
|
|
222
249
|
database_name=cfg["database"],
|
|
223
250
|
custom_name=name,
|
|
251
|
+
ops_database=ops_database,
|
|
224
252
|
)
|
|
225
253
|
|
|
226
254
|
logger.success(f"Generated label: {label}")
|
|
@@ -239,7 +267,11 @@ def backup_incremental(config, baseline_backup, group, name):
|
|
|
239
267
|
)
|
|
240
268
|
|
|
241
269
|
partitions = planner.find_recent_partitions(
|
|
242
|
-
database,
|
|
270
|
+
database,
|
|
271
|
+
cfg["database"],
|
|
272
|
+
baseline_backup_label=baseline_backup,
|
|
273
|
+
group_name=group,
|
|
274
|
+
ops_database=ops_database,
|
|
243
275
|
)
|
|
244
276
|
|
|
245
277
|
if not partitions:
|
|
@@ -252,9 +284,11 @@ def backup_incremental(config, baseline_backup, group, name):
|
|
|
252
284
|
partitions, cfg["repository"], label, cfg["database"]
|
|
253
285
|
)
|
|
254
286
|
|
|
255
|
-
concurrency.reserve_job_slot(
|
|
287
|
+
concurrency.reserve_job_slot(
|
|
288
|
+
database, scope="backup", label=label, ops_database=ops_database
|
|
289
|
+
)
|
|
256
290
|
|
|
257
|
-
planner.record_backup_partitions(database, label, partitions)
|
|
291
|
+
planner.record_backup_partitions(database, label, partitions, ops_database=ops_database)
|
|
258
292
|
|
|
259
293
|
logger.success("Job slot reserved")
|
|
260
294
|
logger.info(f"Starting incremental backup for group '{group}'...")
|
|
@@ -265,6 +299,7 @@ def backup_incremental(config, baseline_backup, group, name):
|
|
|
265
299
|
backup_type="incremental",
|
|
266
300
|
scope="backup",
|
|
267
301
|
database=cfg["database"],
|
|
302
|
+
ops_database=ops_database,
|
|
268
303
|
)
|
|
269
304
|
|
|
270
305
|
if result["success"]:
|
|
@@ -288,7 +323,7 @@ def backup_incremental(config, baseline_backup, group, name):
|
|
|
288
323
|
if state == "LOST":
|
|
289
324
|
logger.critical("Backup tracking lost!")
|
|
290
325
|
logger.warning("Another backup operation started during ours.")
|
|
291
|
-
logger.tip("Enable
|
|
326
|
+
logger.tip("Enable run_status concurrency checks to prevent this.")
|
|
292
327
|
logger.error(f"{result['error_message']}")
|
|
293
328
|
sys.exit(1)
|
|
294
329
|
|
|
@@ -348,13 +383,17 @@ def backup_full(config, group, name):
|
|
|
348
383
|
tls_config=cfg.get("tls"),
|
|
349
384
|
)
|
|
350
385
|
|
|
386
|
+
ops_database = config_module.get_ops_database(cfg)
|
|
387
|
+
|
|
351
388
|
with database:
|
|
352
|
-
was_created = schema.ensure_ops_schema(database)
|
|
389
|
+
was_created = schema.ensure_ops_schema(database, ops_database=ops_database)
|
|
353
390
|
if was_created:
|
|
354
391
|
logger.warning(
|
|
355
392
|
"ops schema was auto-created. Please run 'starrocks-br init' after populating config."
|
|
356
393
|
)
|
|
357
|
-
logger.warning(
|
|
394
|
+
logger.warning(
|
|
395
|
+
"Remember to populate the table_inventory table with your backup groups!"
|
|
396
|
+
)
|
|
358
397
|
sys.exit(1) # Exit if schema was just created, requires user action
|
|
359
398
|
|
|
360
399
|
healthy, message = health.check_cluster_health(database)
|
|
@@ -369,13 +408,25 @@ def backup_full(config, group, name):
|
|
|
369
408
|
logger.success(f"Repository '{cfg['repository']}' verified")
|
|
370
409
|
|
|
371
410
|
label = labels.determine_backup_label(
|
|
372
|
-
db=database,
|
|
411
|
+
db=database,
|
|
412
|
+
backup_type="full",
|
|
413
|
+
database_name=cfg["database"],
|
|
414
|
+
custom_name=name,
|
|
415
|
+
ops_database=ops_database,
|
|
373
416
|
)
|
|
374
417
|
|
|
375
418
|
logger.success(f"Generated label: {label}")
|
|
376
419
|
|
|
420
|
+
tables = planner.find_tables_by_group(database, group, ops_database)
|
|
421
|
+
planner.validate_tables_exist(database, cfg["database"], tables, group)
|
|
422
|
+
|
|
377
423
|
backup_command = planner.build_full_backup_command(
|
|
378
|
-
database,
|
|
424
|
+
database,
|
|
425
|
+
group,
|
|
426
|
+
cfg["repository"],
|
|
427
|
+
label,
|
|
428
|
+
cfg["database"],
|
|
429
|
+
ops_database=ops_database,
|
|
379
430
|
)
|
|
380
431
|
|
|
381
432
|
if not backup_command:
|
|
@@ -389,9 +440,13 @@ def backup_full(config, group, name):
|
|
|
389
440
|
database, cfg["database"], tables
|
|
390
441
|
)
|
|
391
442
|
|
|
392
|
-
concurrency.reserve_job_slot(
|
|
443
|
+
concurrency.reserve_job_slot(
|
|
444
|
+
database, scope="backup", label=label, ops_database=ops_database
|
|
445
|
+
)
|
|
393
446
|
|
|
394
|
-
planner.record_backup_partitions(
|
|
447
|
+
planner.record_backup_partitions(
|
|
448
|
+
database, label, all_partitions, ops_database=ops_database
|
|
449
|
+
)
|
|
395
450
|
|
|
396
451
|
logger.success("Job slot reserved")
|
|
397
452
|
logger.info(f"Starting full backup for group '{group}'...")
|
|
@@ -402,6 +457,7 @@ def backup_full(config, group, name):
|
|
|
402
457
|
backup_type="full",
|
|
403
458
|
scope="backup",
|
|
404
459
|
database=cfg["database"],
|
|
460
|
+
ops_database=ops_database,
|
|
405
461
|
)
|
|
406
462
|
|
|
407
463
|
if result["success"]:
|
|
@@ -419,10 +475,13 @@ def backup_full(config, group, name):
|
|
|
419
475
|
if state == "LOST":
|
|
420
476
|
logger.critical("Backup tracking lost!")
|
|
421
477
|
logger.warning("Another backup operation started during ours.")
|
|
422
|
-
logger.tip("Enable
|
|
478
|
+
logger.tip("Enable run_status concurrency checks to prevent this.")
|
|
423
479
|
logger.error(f"{result['error_message']}")
|
|
424
480
|
sys.exit(1)
|
|
425
481
|
|
|
482
|
+
except exceptions.InvalidTablesInInventoryError as e:
|
|
483
|
+
error_handler.handle_invalid_tables_in_inventory_error(e, config)
|
|
484
|
+
sys.exit(1)
|
|
426
485
|
except exceptions.ConcurrencyConflictError as e:
|
|
427
486
|
error_handler.handle_concurrency_conflict_error(e, config)
|
|
428
487
|
sys.exit(1)
|
|
@@ -499,13 +558,17 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
|
|
|
499
558
|
tls_config=cfg.get("tls"),
|
|
500
559
|
)
|
|
501
560
|
|
|
561
|
+
ops_database = config_module.get_ops_database(cfg)
|
|
562
|
+
|
|
502
563
|
with database:
|
|
503
|
-
was_created = schema.ensure_ops_schema(database)
|
|
564
|
+
was_created = schema.ensure_ops_schema(database, ops_database=ops_database)
|
|
504
565
|
if was_created:
|
|
505
566
|
logger.warning(
|
|
506
567
|
"ops schema was auto-created. Please run 'starrocks-br init' after populating config."
|
|
507
568
|
)
|
|
508
|
-
logger.warning(
|
|
569
|
+
logger.warning(
|
|
570
|
+
"Remember to populate the table_inventory table with your backup groups!"
|
|
571
|
+
)
|
|
509
572
|
sys.exit(1) # Exit if schema was just created, requires user action
|
|
510
573
|
|
|
511
574
|
healthy, message = health.check_cluster_health(database)
|
|
@@ -521,7 +584,9 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
|
|
|
521
584
|
|
|
522
585
|
logger.info(f"Finding restore sequence for target backup: {target_label}")
|
|
523
586
|
|
|
524
|
-
restore_pair = restore.find_restore_pair(
|
|
587
|
+
restore_pair = restore.find_restore_pair(
|
|
588
|
+
database, target_label, ops_database=ops_database
|
|
589
|
+
)
|
|
525
590
|
logger.success(f"Found restore sequence: {' -> '.join(restore_pair)}")
|
|
526
591
|
|
|
527
592
|
logger.info("Determining tables to restore from backup manifest...")
|
|
@@ -532,6 +597,7 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
|
|
|
532
597
|
group=group,
|
|
533
598
|
table=table,
|
|
534
599
|
database=cfg["database"] if table else None,
|
|
600
|
+
ops_database=ops_database,
|
|
535
601
|
)
|
|
536
602
|
|
|
537
603
|
if not tables_to_restore:
|
|
@@ -549,6 +615,7 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
|
|
|
549
615
|
tables_to_restore,
|
|
550
616
|
rename_suffix,
|
|
551
617
|
skip_confirmation=yes,
|
|
618
|
+
ops_database=ops_database,
|
|
552
619
|
)
|
|
553
620
|
|
|
554
621
|
if result["success"]:
|
|
@@ -17,45 +17,51 @@ from typing import Literal
|
|
|
17
17
|
from . import exceptions, logger, utils
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def reserve_job_slot(db, scope: str, label: str) -> None:
|
|
21
|
-
"""Reserve a job slot in
|
|
20
|
+
def reserve_job_slot(db, scope: str, label: str, ops_database: str = "ops") -> None:
|
|
21
|
+
"""Reserve a job slot in the run_status table to prevent overlapping jobs.
|
|
22
22
|
|
|
23
23
|
We consider any row with state='ACTIVE' for the same scope as a conflict.
|
|
24
24
|
However, we implement self-healing logic to automatically clean up stale locks.
|
|
25
25
|
"""
|
|
26
|
-
active_jobs = _get_active_jobs_for_scope(db, scope)
|
|
26
|
+
active_jobs = _get_active_jobs_for_scope(db, scope, ops_database)
|
|
27
27
|
|
|
28
28
|
if not active_jobs:
|
|
29
|
-
_insert_new_job(db, scope, label)
|
|
29
|
+
_insert_new_job(db, scope, label, ops_database)
|
|
30
30
|
return
|
|
31
31
|
|
|
32
|
-
_handle_active_job_conflicts(db, scope, active_jobs)
|
|
32
|
+
_handle_active_job_conflicts(db, scope, active_jobs, ops_database)
|
|
33
33
|
|
|
34
|
-
_insert_new_job(db, scope, label)
|
|
34
|
+
_insert_new_job(db, scope, label, ops_database)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def _get_active_jobs_for_scope(
|
|
37
|
+
def _get_active_jobs_for_scope(
|
|
38
|
+
db, scope: str, ops_database: str = "ops"
|
|
39
|
+
) -> list[tuple[str, str, str]]:
|
|
38
40
|
"""Get all active jobs for the given scope."""
|
|
39
|
-
rows = db.query(
|
|
41
|
+
rows = db.query(
|
|
42
|
+
f"SELECT scope, label, state FROM {ops_database}.run_status WHERE state = 'ACTIVE'"
|
|
43
|
+
)
|
|
40
44
|
return [row for row in rows if row[0] == scope]
|
|
41
45
|
|
|
42
46
|
|
|
43
|
-
def _handle_active_job_conflicts(
|
|
47
|
+
def _handle_active_job_conflicts(
|
|
48
|
+
db, scope: str, active_jobs: list[tuple[str, str, str]], ops_database: str = "ops"
|
|
49
|
+
) -> None:
|
|
44
50
|
"""Handle conflicts with active jobs, cleaning up stale ones where possible."""
|
|
45
51
|
for active_scope, active_label, _ in active_jobs:
|
|
46
|
-
if _can_heal_stale_job(active_scope, active_label, db):
|
|
47
|
-
_cleanup_stale_job(db, active_scope, active_label)
|
|
52
|
+
if _can_heal_stale_job(active_scope, active_label, db, ops_database):
|
|
53
|
+
_cleanup_stale_job(db, active_scope, active_label, ops_database)
|
|
48
54
|
logger.success(f"Cleaned up stale backup job: {active_label}")
|
|
49
55
|
else:
|
|
50
56
|
_raise_concurrency_conflict(scope, active_jobs)
|
|
51
57
|
|
|
52
58
|
|
|
53
|
-
def _can_heal_stale_job(scope: str, label: str, db) -> bool:
|
|
59
|
+
def _can_heal_stale_job(scope: str, label: str, db, ops_database: str = "ops") -> bool:
|
|
54
60
|
"""Check if a stale job can be healed (only for backup jobs)."""
|
|
55
61
|
if scope != "backup":
|
|
56
62
|
return False
|
|
57
63
|
|
|
58
|
-
return _is_backup_job_stale(db, label)
|
|
64
|
+
return _is_backup_job_stale(db, label, ops_database)
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
def _raise_concurrency_conflict(scope: str, active_jobs: list[tuple[str, str, str]]) -> None:
|
|
@@ -63,22 +69,22 @@ def _raise_concurrency_conflict(scope: str, active_jobs: list[tuple[str, str, st
|
|
|
63
69
|
raise exceptions.ConcurrencyConflictError(scope, active_jobs)
|
|
64
70
|
|
|
65
71
|
|
|
66
|
-
def _insert_new_job(db, scope: str, label: str) -> None:
|
|
72
|
+
def _insert_new_job(db, scope: str, label: str, ops_database: str = "ops") -> None:
|
|
67
73
|
"""Insert a new active job record."""
|
|
68
74
|
sql = f"""
|
|
69
|
-
INSERT INTO
|
|
75
|
+
INSERT INTO {ops_database}.run_status (scope, label, state, started_at)
|
|
70
76
|
VALUES ({utils.quote_value(scope)}, {utils.quote_value(label)}, 'ACTIVE', NOW())
|
|
71
77
|
"""
|
|
72
78
|
db.execute(sql)
|
|
73
79
|
|
|
74
80
|
|
|
75
|
-
def _is_backup_job_stale(db, label: str) -> bool:
|
|
81
|
+
def _is_backup_job_stale(db, label: str, ops_database: str = "ops") -> bool:
|
|
76
82
|
"""Check if a backup job is stale by querying StarRocks SHOW BACKUP.
|
|
77
83
|
|
|
78
84
|
Returns True if the job is stale (not actually running), False if it's still active.
|
|
79
85
|
"""
|
|
80
86
|
try:
|
|
81
|
-
user_databases = _get_user_databases(db)
|
|
87
|
+
user_databases = _get_user_databases(db, ops_database)
|
|
82
88
|
|
|
83
89
|
for database_name in user_databases:
|
|
84
90
|
job_status = _check_backup_job_in_database(db, database_name, label)
|
|
@@ -98,9 +104,9 @@ def _is_backup_job_stale(db, label: str) -> bool:
|
|
|
98
104
|
return False
|
|
99
105
|
|
|
100
106
|
|
|
101
|
-
def _get_user_databases(db) -> list[str]:
|
|
107
|
+
def _get_user_databases(db, ops_database: str = "ops") -> list[str]:
|
|
102
108
|
"""Get list of user databases (excluding system databases)."""
|
|
103
|
-
system_databases = {"information_schema", "mysql", "sys",
|
|
109
|
+
system_databases = {"information_schema", "mysql", "sys", ops_database}
|
|
104
110
|
|
|
105
111
|
databases = db.query("SHOW DATABASES")
|
|
106
112
|
return [
|
|
@@ -159,10 +165,10 @@ def _extract_backup_info(result) -> tuple[str, str]:
|
|
|
159
165
|
return snapshot_name, state
|
|
160
166
|
|
|
161
167
|
|
|
162
|
-
def _cleanup_stale_job(db, scope: str, label: str) -> None:
|
|
168
|
+
def _cleanup_stale_job(db, scope: str, label: str, ops_database: str = "ops") -> None:
|
|
163
169
|
"""Clean up a stale job by updating its state to CANCELLED."""
|
|
164
170
|
sql = f"""
|
|
165
|
-
UPDATE
|
|
171
|
+
UPDATE {ops_database}.run_status
|
|
166
172
|
SET state='CANCELLED', finished_at=NOW()
|
|
167
173
|
WHERE scope={utils.quote_value(scope)} AND label={utils.quote_value(label)} AND state='ACTIVE'
|
|
168
174
|
"""
|
|
@@ -170,14 +176,18 @@ def _cleanup_stale_job(db, scope: str, label: str) -> None:
|
|
|
170
176
|
|
|
171
177
|
|
|
172
178
|
def complete_job_slot(
|
|
173
|
-
db,
|
|
179
|
+
db,
|
|
180
|
+
scope: str,
|
|
181
|
+
label: str,
|
|
182
|
+
final_state: Literal["FINISHED", "FAILED", "CANCELLED"],
|
|
183
|
+
ops_database: str = "ops",
|
|
174
184
|
) -> None:
|
|
175
185
|
"""Complete job slot and persist final state.
|
|
176
186
|
|
|
177
187
|
Simple approach: update the same row by scope/label.
|
|
178
188
|
"""
|
|
179
189
|
sql = f"""
|
|
180
|
-
UPDATE
|
|
190
|
+
UPDATE {ops_database}.run_status
|
|
181
191
|
SET state={utils.quote_value(final_state)}, finished_at=NOW()
|
|
182
192
|
WHERE scope={utils.quote_value(scope)} AND label={utils.quote_value(label)}
|
|
183
193
|
"""
|
|
@@ -57,6 +57,34 @@ def validate_config(config: dict[str, Any]) -> None:
|
|
|
57
57
|
raise exceptions.ConfigValidationError(f"Missing required config field: {field}")
|
|
58
58
|
|
|
59
59
|
_validate_tls_section(config.get("tls"))
|
|
60
|
+
_validate_table_inventory_section(config.get("table_inventory"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_ops_database(config: dict[str, Any]) -> str:
|
|
64
|
+
"""Get the ops database name from config, defaulting to 'ops'."""
|
|
65
|
+
return config.get("ops_database", "ops")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_table_inventory_entries(config: dict[str, Any]) -> list[tuple[str, str, str]]:
|
|
69
|
+
"""Extract table inventory entries from config.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
config: Configuration dictionary
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of tuples (group, database, table)
|
|
76
|
+
"""
|
|
77
|
+
table_inventory = config.get("table_inventory")
|
|
78
|
+
if not table_inventory:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
entries = []
|
|
82
|
+
for group_entry in table_inventory:
|
|
83
|
+
group = group_entry["group"]
|
|
84
|
+
for table_entry in group_entry["tables"]:
|
|
85
|
+
entries.append((group, table_entry["database"], table_entry["table"]))
|
|
86
|
+
|
|
87
|
+
return entries
|
|
60
88
|
|
|
61
89
|
|
|
62
90
|
def _validate_tls_section(tls_config) -> None:
|
|
@@ -88,3 +116,50 @@ def _validate_tls_section(tls_config) -> None:
|
|
|
88
116
|
raise exceptions.ConfigValidationError(
|
|
89
117
|
"TLS configuration field 'tls_versions' must be a list of strings if provided"
|
|
90
118
|
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _validate_table_inventory_section(table_inventory) -> None:
|
|
122
|
+
if table_inventory is None:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if not isinstance(table_inventory, list):
|
|
126
|
+
raise exceptions.ConfigValidationError("'table_inventory' must be a list")
|
|
127
|
+
|
|
128
|
+
for entry in table_inventory:
|
|
129
|
+
if not isinstance(entry, dict):
|
|
130
|
+
raise exceptions.ConfigValidationError(
|
|
131
|
+
"Each entry in 'table_inventory' must be a dictionary"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if "group" not in entry:
|
|
135
|
+
raise exceptions.ConfigValidationError(
|
|
136
|
+
"Each entry in 'table_inventory' must have a 'group' field"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if "tables" not in entry:
|
|
140
|
+
raise exceptions.ConfigValidationError(
|
|
141
|
+
"Each entry in 'table_inventory' must have a 'tables' field"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if not isinstance(entry["group"], str):
|
|
145
|
+
raise exceptions.ConfigValidationError("'group' field must be a string")
|
|
146
|
+
|
|
147
|
+
tables = entry["tables"]
|
|
148
|
+
if not isinstance(tables, list):
|
|
149
|
+
raise exceptions.ConfigValidationError("'tables' field must be a list")
|
|
150
|
+
|
|
151
|
+
for table_entry in tables:
|
|
152
|
+
if not isinstance(table_entry, dict):
|
|
153
|
+
raise exceptions.ConfigValidationError("Each table entry must be a dictionary")
|
|
154
|
+
|
|
155
|
+
if "database" not in table_entry or "table" not in table_entry:
|
|
156
|
+
raise exceptions.ConfigValidationError(
|
|
157
|
+
"Each table entry must have 'database' and 'table' fields"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if not isinstance(table_entry["database"], str) or not isinstance(
|
|
161
|
+
table_entry["table"], str
|
|
162
|
+
):
|
|
163
|
+
raise exceptions.ConfigValidationError(
|
|
164
|
+
"'database' and 'table' fields must be strings"
|
|
165
|
+
)
|