Spaces:
Running
Running
mike boone commited on
Commit Β·
cdded3f
1
Parent(s): 20bd973
fix: harden ThoughtSpot table import recovery
Browse files- thoughtspot_deployer.py +73 -13
thoughtspot_deployer.py
CHANGED
|
@@ -2217,6 +2217,15 @@ class ThoughtSpotDeployer:
|
|
| 2217 |
return result['object']
|
| 2218 |
return None
|
| 2219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2220 |
def _import_tml_chunk(phase_label, names, tmls, create_new):
|
| 2221 |
payload = {
|
| 2222 |
"metadata_tmls": tmls,
|
|
@@ -2243,6 +2252,25 @@ class ThoughtSpotDeployer:
|
|
| 2243 |
)
|
| 2244 |
elapsed = time.time() - start
|
| 2245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2246 |
if response.status_code == 200:
|
| 2247 |
objects = _normalize_tml_import_objects(response.json())
|
| 2248 |
if objects is None:
|
|
@@ -2337,7 +2365,7 @@ class ThoughtSpotDeployer:
|
|
| 2337 |
results['warnings'].append(message)
|
| 2338 |
|
| 2339 |
def _import_tmls_chunked(phase_label, names, tmls, create_new, chunk_size=3, fatal_errors=True):
|
| 2340 |
-
all_objects =
|
| 2341 |
retriable_statuses = {502, 503, 504}
|
| 2342 |
|
| 2343 |
def _retry_single_table_after_gateway_timeout(single_name, single_tml, max_attempts=3):
|
|
@@ -2375,9 +2403,7 @@ class ThoughtSpotDeployer:
|
|
| 2375 |
if isinstance(result, requests.Response) and result.status_code in retriable_statuses:
|
| 2376 |
if create_new:
|
| 2377 |
resolved = _resolve_existing_tables_after_timeout(chunk_names)
|
| 2378 |
-
|
| 2379 |
-
if name in resolved:
|
| 2380 |
-
all_objects.append(resolved[name])
|
| 2381 |
missing = [
|
| 2382 |
(name, tml)
|
| 2383 |
for name, tml in zip(chunk_names, chunk_tmls)
|
|
@@ -2416,7 +2442,7 @@ class ThoughtSpotDeployer:
|
|
| 2416 |
if fatal_errors:
|
| 2417 |
return None
|
| 2418 |
continue
|
| 2419 |
-
all_objects.
|
| 2420 |
continue
|
| 2421 |
|
| 2422 |
log_progress(f" β οΈ {chunk_label} timed out; retrying once")
|
|
@@ -2433,7 +2459,7 @@ class ThoughtSpotDeployer:
|
|
| 2433 |
if create_new and len(chunk_names) == 1:
|
| 2434 |
resolved = _resolve_existing_tables_after_timeout(chunk_names)
|
| 2435 |
if chunk_names[0] in resolved:
|
| 2436 |
-
all_objects
|
| 2437 |
continue
|
| 2438 |
error = (
|
| 2439 |
f"{phase_label} failed: HTTP {result.status_code} - {result.text}"
|
|
@@ -2448,7 +2474,7 @@ class ThoughtSpotDeployer:
|
|
| 2448 |
if fatal_errors:
|
| 2449 |
return None
|
| 2450 |
continue
|
| 2451 |
-
all_objects.
|
| 2452 |
|
| 2453 |
return all_objects
|
| 2454 |
|
|
@@ -2466,19 +2492,27 @@ class ThoughtSpotDeployer:
|
|
| 2466 |
# Send tables in small chunks. ThoughtSpot can intermittently 504
|
| 2467 |
# large metadata imports at the gateway even for valid table TML.
|
| 2468 |
log_progress(f" Sending table creation requests for {len(table_tmls_batch1)} tables...")
|
|
|
|
| 2469 |
objects = _import_tmls_chunked(
|
| 2470 |
"Batch 1",
|
| 2471 |
table_names_order,
|
| 2472 |
table_tmls_batch1,
|
| 2473 |
create_new=True,
|
|
|
|
| 2474 |
)
|
| 2475 |
|
| 2476 |
if objects is None:
|
| 2477 |
return results
|
| 2478 |
|
| 2479 |
-
# Process each table result
|
| 2480 |
-
|
| 2481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2482 |
|
| 2483 |
if obj.get('response', {}).get('status', {}).get('status_code') == 'OK':
|
| 2484 |
table_guid = obj.get('response', {}).get('header', {}).get('id_guid')
|
|
@@ -2504,6 +2538,25 @@ class ThoughtSpotDeployer:
|
|
| 2504 |
log_progress(" β No tables were created successfully in Batch 1")
|
| 2505 |
return results
|
| 2506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2507 |
# Assign tags to tables
|
| 2508 |
table_guid_list = list(table_guids.values())
|
| 2509 |
print(f"π DEBUG BEFORE TAG CALL: tag_name='{tag_name}', table_guid_list={table_guid_list}")
|
|
@@ -2552,13 +2605,20 @@ class ThoughtSpotDeployer:
|
|
| 2552 |
table_names_order_batch2,
|
| 2553 |
table_tmls_batch2,
|
| 2554 |
create_new=False,
|
|
|
|
| 2555 |
fatal_errors=False,
|
| 2556 |
)
|
| 2557 |
|
| 2558 |
if objects is not None:
|
| 2559 |
-
# Process each result
|
| 2560 |
-
|
| 2561 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2562 |
|
| 2563 |
if obj.get('response', {}).get('status', {}).get('status_code') == 'OK':
|
| 2564 |
print(f"[ThoughtSpot] β
{table_name} joins added", flush=True)
|
|
|
|
| 2217 |
return result['object']
|
| 2218 |
return None
|
| 2219 |
|
| 2220 |
+
def _tml_objects_by_name(expected_names, objects):
|
| 2221 |
+
named = {}
|
| 2222 |
+
for idx, obj in enumerate(objects or []):
|
| 2223 |
+
header = obj.get('response', {}).get('header', {}) if isinstance(obj, dict) else {}
|
| 2224 |
+
raw_name = header.get('name') or header.get('display_name')
|
| 2225 |
+
table_name = (raw_name or (expected_names[idx] if idx < len(expected_names) else f"TABLE_{idx}")).upper()
|
| 2226 |
+
named[table_name] = obj
|
| 2227 |
+
return named
|
| 2228 |
+
|
| 2229 |
def _import_tml_chunk(phase_label, names, tmls, create_new):
|
| 2230 |
payload = {
|
| 2231 |
"metadata_tmls": tmls,
|
|
|
|
| 2252 |
)
|
| 2253 |
elapsed = time.time() - start
|
| 2254 |
|
| 2255 |
+
if response.status_code == 401:
|
| 2256 |
+
log_progress(f" β οΈ {phase_label} auth expired; re-authenticating and retrying once")
|
| 2257 |
+
if _slog:
|
| 2258 |
+
_slog.log(
|
| 2259 |
+
"thoughtspot",
|
| 2260 |
+
"tml import auth expired",
|
| 2261 |
+
phase=phase_label,
|
| 2262 |
+
table_names=names,
|
| 2263 |
+
elapsed_s=round(elapsed, 1),
|
| 2264 |
+
)
|
| 2265 |
+
if self.authenticate():
|
| 2266 |
+
retry_start = time.time()
|
| 2267 |
+
response = self.session.post(
|
| 2268 |
+
f"{self.base_url}/api/rest/2.0/metadata/tml/import",
|
| 2269 |
+
json=payload,
|
| 2270 |
+
timeout=360,
|
| 2271 |
+
)
|
| 2272 |
+
elapsed += time.time() - retry_start
|
| 2273 |
+
|
| 2274 |
if response.status_code == 200:
|
| 2275 |
objects = _normalize_tml_import_objects(response.json())
|
| 2276 |
if objects is None:
|
|
|
|
| 2365 |
results['warnings'].append(message)
|
| 2366 |
|
| 2367 |
def _import_tmls_chunked(phase_label, names, tmls, create_new, chunk_size=3, fatal_errors=True):
|
| 2368 |
+
all_objects = {}
|
| 2369 |
retriable_statuses = {502, 503, 504}
|
| 2370 |
|
| 2371 |
def _retry_single_table_after_gateway_timeout(single_name, single_tml, max_attempts=3):
|
|
|
|
| 2403 |
if isinstance(result, requests.Response) and result.status_code in retriable_statuses:
|
| 2404 |
if create_new:
|
| 2405 |
resolved = _resolve_existing_tables_after_timeout(chunk_names)
|
| 2406 |
+
all_objects.update(resolved)
|
|
|
|
|
|
|
| 2407 |
missing = [
|
| 2408 |
(name, tml)
|
| 2409 |
for name, tml in zip(chunk_names, chunk_tmls)
|
|
|
|
| 2442 |
if fatal_errors:
|
| 2443 |
return None
|
| 2444 |
continue
|
| 2445 |
+
all_objects.update(_tml_objects_by_name([single_name], single_result))
|
| 2446 |
continue
|
| 2447 |
|
| 2448 |
log_progress(f" β οΈ {chunk_label} timed out; retrying once")
|
|
|
|
| 2459 |
if create_new and len(chunk_names) == 1:
|
| 2460 |
resolved = _resolve_existing_tables_after_timeout(chunk_names)
|
| 2461 |
if chunk_names[0] in resolved:
|
| 2462 |
+
all_objects[chunk_names[0]] = resolved[chunk_names[0]]
|
| 2463 |
continue
|
| 2464 |
error = (
|
| 2465 |
f"{phase_label} failed: HTTP {result.status_code} - {result.text}"
|
|
|
|
| 2474 |
if fatal_errors:
|
| 2475 |
return None
|
| 2476 |
continue
|
| 2477 |
+
all_objects.update(_tml_objects_by_name(chunk_names, result))
|
| 2478 |
|
| 2479 |
return all_objects
|
| 2480 |
|
|
|
|
| 2492 |
# Send tables in small chunks. ThoughtSpot can intermittently 504
|
| 2493 |
# large metadata imports at the gateway even for valid table TML.
|
| 2494 |
log_progress(f" Sending table creation requests for {len(table_tmls_batch1)} tables...")
|
| 2495 |
+
table_import_chunk_size = 1 if table_count >= 7 else 3
|
| 2496 |
objects = _import_tmls_chunked(
|
| 2497 |
"Batch 1",
|
| 2498 |
table_names_order,
|
| 2499 |
table_tmls_batch1,
|
| 2500 |
create_new=True,
|
| 2501 |
+
chunk_size=table_import_chunk_size,
|
| 2502 |
)
|
| 2503 |
|
| 2504 |
if objects is None:
|
| 2505 |
return results
|
| 2506 |
|
| 2507 |
+
# Process each table result by table name. Gateway-timeout recovery can
|
| 2508 |
+
# return a partial set, so positional matching is unsafe here.
|
| 2509 |
+
for table_name in table_names_order:
|
| 2510 |
+
obj = objects.get(table_name)
|
| 2511 |
+
if obj is None:
|
| 2512 |
+
error = f"Table {table_name} failed: no import response after retry"
|
| 2513 |
+
print(f"[ThoughtSpot] β {error}", flush=True)
|
| 2514 |
+
results['errors'].append(error)
|
| 2515 |
+
continue
|
| 2516 |
|
| 2517 |
if obj.get('response', {}).get('status', {}).get('status_code') == 'OK':
|
| 2518 |
table_guid = obj.get('response', {}).get('header', {}).get('id_guid')
|
|
|
|
| 2538 |
log_progress(" β No tables were created successfully in Batch 1")
|
| 2539 |
return results
|
| 2540 |
|
| 2541 |
+
missing_tables = [name for name in table_names_order if name not in table_guids]
|
| 2542 |
+
if missing_tables:
|
| 2543 |
+
error = (
|
| 2544 |
+
f"Batch 1 incomplete: {len(table_guids)}/{len(table_names_order)} "
|
| 2545 |
+
f"tables created; missing {', '.join(missing_tables)}"
|
| 2546 |
+
)
|
| 2547 |
+
log_progress(f" β {error}")
|
| 2548 |
+
results['errors'].append(error)
|
| 2549 |
+
if _slog:
|
| 2550 |
+
_slog.log(
|
| 2551 |
+
"thoughtspot",
|
| 2552 |
+
"table creation incomplete",
|
| 2553 |
+
error=error,
|
| 2554 |
+
table_count=len(table_names_order),
|
| 2555 |
+
created_count=len(table_guids),
|
| 2556 |
+
missing_tables=missing_tables,
|
| 2557 |
+
)
|
| 2558 |
+
return results
|
| 2559 |
+
|
| 2560 |
# Assign tags to tables
|
| 2561 |
table_guid_list = list(table_guids.values())
|
| 2562 |
print(f"π DEBUG BEFORE TAG CALL: tag_name='{tag_name}', table_guid_list={table_guid_list}")
|
|
|
|
| 2605 |
table_names_order_batch2,
|
| 2606 |
table_tmls_batch2,
|
| 2607 |
create_new=False,
|
| 2608 |
+
chunk_size=table_import_chunk_size,
|
| 2609 |
fatal_errors=False,
|
| 2610 |
)
|
| 2611 |
|
| 2612 |
if objects is not None:
|
| 2613 |
+
# Process each result by table name. Partial retry recovery can
|
| 2614 |
+
# return fewer objects than requested.
|
| 2615 |
+
for table_name in table_names_order_batch2:
|
| 2616 |
+
obj = objects.get(table_name)
|
| 2617 |
+
if obj is None:
|
| 2618 |
+
warning = f"Joins for {table_name} failed: no import response after retry"
|
| 2619 |
+
print(f"[ThoughtSpot] β οΈ {warning}", flush=True)
|
| 2620 |
+
results['warnings'].append(warning)
|
| 2621 |
+
continue
|
| 2622 |
|
| 2623 |
if obj.get('response', {}).get('status', {}).get('status_code') == 'OK':
|
| 2624 |
print(f"[ThoughtSpot] β
{table_name} joins added", flush=True)
|