mike boone commited on
Commit
cdded3f
Β·
1 Parent(s): 20bd973

fix: harden ThoughtSpot table import recovery

Browse files
Files changed (1) hide show
  1. 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
- for name in chunk_names:
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.extend(single_result)
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.append(resolved[chunk_names[0]])
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.extend(result)
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
- for idx, obj in enumerate(objects):
2481
- table_name = table_names_order[idx] if idx < len(table_names_order) else f"TABLE_{idx}"
 
 
 
 
 
 
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
- for idx, obj in enumerate(objects):
2561
- table_name = table_names_order_batch2[idx] if idx < len(table_names_order_batch2) else f"TABLE_{idx}"
 
 
 
 
 
 
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)