2
2
import logging
3
3
import re
4
4
import time
5
- from collections import OrderedDict
6
- from dataclasses import dataclass
7
- from datetime import datetime
5
+ from collections import OrderedDict , defaultdict
6
+ from dataclasses import dataclass , field as dataclass_field
7
+ from datetime import datetime , timedelta , timezone
8
8
from functools import lru_cache
9
9
from typing import (
10
10
Any ,
196
196
504 , # Gateway Timeout
197
197
]
198
198
199
+ # From experience, this expiry time typically ranges from 50 minutes
200
+ # to 2 hours but might as well be configurable. We will allow upto
201
+ # 10 minutes of such expiry time
202
+ REGULAR_AUTH_EXPIRY_PERIOD = timedelta (minutes = 10 )
203
+
199
204
logger : logging .Logger = logging .getLogger (__name__ )
200
205
201
206
# Replace / with |
@@ -637,6 +642,7 @@ class SiteIdContentUrl:
637
642
site_content_url : str
638
643
639
644
645
+ @dataclass
640
646
class TableauSourceReport (StaleEntityRemovalSourceReport ):
641
647
get_all_datasources_query_failed : bool = False
642
648
num_get_datasource_query_failures : int = 0
@@ -653,7 +659,14 @@ class TableauSourceReport(StaleEntityRemovalSourceReport):
653
659
num_upstream_table_lineage_failed_parse_sql : int = 0
654
660
num_upstream_fine_grained_lineage_failed_parse_sql : int = 0
655
661
num_hidden_assets_skipped : int = 0
656
- logged_in_user : List [UserInfo ] = []
662
+ logged_in_user : List [UserInfo ] = dataclass_field (default_factory = list )
663
+ last_authenticated_at : Optional [datetime ] = None
664
+
665
+ num_expected_tableau_metadata_queries : int = 0
666
+ num_actual_tableau_metadata_queries : int = 0
667
+ tableau_server_error_stats : Dict [str , int ] = dataclass_field (
668
+ default_factory = (lambda : defaultdict (int ))
669
+ )
657
670
658
671
659
672
def report_user_role (report : TableauSourceReport , server : Server ) -> None :
@@ -724,6 +737,7 @@ def _authenticate(self, site_content_url: str) -> None:
724
737
try :
725
738
logger .info (f"Authenticated to Tableau site: '{ site_content_url } '" )
726
739
self .server = self .config .make_tableau_client (site_content_url )
740
+ self .report .last_authenticated_at = datetime .now (timezone .utc )
727
741
report_user_role (report = self .report , server = self .server )
728
742
# Note that we're not catching ConfigurationError, since we want that to throw.
729
743
except ValueError as e :
@@ -807,10 +821,13 @@ def get_workunits_internal(self) -> Iterable[MetadataWorkUnit]:
807
821
site_source = TableauSiteSource (
808
822
config = self .config ,
809
823
ctx = self .ctx ,
810
- site = site
811
- if site
812
- else SiteIdContentUrl (
813
- site_id = self .server .site_id , site_content_url = self .config .site
824
+ site = (
825
+ site
826
+ if site
827
+ else SiteIdContentUrl (
828
+ site_id = self .server .site_id ,
829
+ site_content_url = self .config .site ,
830
+ )
814
831
),
815
832
report = self .report ,
816
833
server = self .server ,
@@ -925,6 +942,7 @@ def _re_authenticate(self) -> None:
925
942
# Sign-in again may not be enough because Tableau sometimes caches invalid sessions
926
943
# so we need to recreate the Tableau Server object
927
944
self .server = self .config .make_tableau_client (self .site_content_url )
945
+ self .report .last_authenticated_at = datetime .now (timezone .utc )
928
946
929
947
def _populate_usage_stat_registry (self ) -> None :
930
948
if self .server is None :
@@ -1190,6 +1208,7 @@ def get_connection_object_page(
1190
1208
)
1191
1209
try :
1192
1210
assert self .server is not None
1211
+ self .report .num_actual_tableau_metadata_queries += 1
1193
1212
query_data = query_metadata_cursor_based_pagination (
1194
1213
server = self .server ,
1195
1214
main_query = query ,
@@ -1199,25 +1218,36 @@ def get_connection_object_page(
1199
1218
qry_filter = query_filter ,
1200
1219
)
1201
1220
1202
- except REAUTHENTICATE_ERRORS :
1203
- if not retry_on_auth_error :
1221
+ except REAUTHENTICATE_ERRORS as e :
1222
+ self .report .tableau_server_error_stats [e .__class__ .__name__ ] += 1
1223
+ if not retry_on_auth_error or retries_remaining <= 0 :
1204
1224
raise
1205
1225
1206
- # If ingestion has been running for over 2 hours, the Tableau
1207
- # temporary credentials will expire. If this happens, this exception
1208
- # will be thrown, and we need to re-authenticate and retry.
1209
- self ._re_authenticate ()
1226
+ # We have been getting some irregular authorization errors like below well before the expected expiry time
1227
+ # - within few seconds of initial authentication . We'll retry without re-auth for such cases.
1228
+ # <class 'tableauserverclient.server.endpoint.exceptions.NonXMLResponseError'>:
1229
+ # b'{"timestamp":"xxx","status":401,"error":"Unauthorized","path":"/relationship-service-war/graphql"}'
1230
+ if self .report .last_authenticated_at and (
1231
+ datetime .now (timezone .utc ) - self .report .last_authenticated_at
1232
+ > REGULAR_AUTH_EXPIRY_PERIOD
1233
+ ):
1234
+ # If ingestion has been running for over 2 hours, the Tableau
1235
+ # temporary credentials will expire. If this happens, this exception
1236
+ # will be thrown, and we need to re-authenticate and retry.
1237
+ self ._re_authenticate ()
1238
+
1210
1239
return self .get_connection_object_page (
1211
1240
query = query ,
1212
1241
connection_type = connection_type ,
1213
1242
query_filter = query_filter ,
1214
1243
fetch_size = fetch_size ,
1215
1244
current_cursor = current_cursor ,
1216
- retry_on_auth_error = False ,
1245
+ retry_on_auth_error = True ,
1217
1246
retries_remaining = retries_remaining - 1 ,
1218
1247
)
1219
1248
1220
1249
except InternalServerError as ise :
1250
+ self .report .tableau_server_error_stats [InternalServerError .__name__ ] += 1
1221
1251
# In some cases Tableau Server returns 504 error, which is a timeout error, so it worths to retry.
1222
1252
# Extended with other retryable errors.
1223
1253
if ise .code in RETRIABLE_ERROR_CODES :
@@ -1230,13 +1260,14 @@ def get_connection_object_page(
1230
1260
query_filter = query_filter ,
1231
1261
fetch_size = fetch_size ,
1232
1262
current_cursor = current_cursor ,
1233
- retry_on_auth_error = False ,
1263
+ retry_on_auth_error = True ,
1234
1264
retries_remaining = retries_remaining - 1 ,
1235
1265
)
1236
1266
else :
1237
1267
raise ise
1238
1268
1239
1269
except OSError :
1270
+ self .report .tableau_server_error_stats [OSError .__name__ ] += 1
1240
1271
# In tableauseverclient 0.26 (which was yanked and released in 0.28 on 2023-10-04),
1241
1272
# the request logic was changed to use threads.
1242
1273
# https://github.com/tableau/server-client-python/commit/307d8a20a30f32c1ce615cca7c6a78b9b9bff081
@@ -1251,7 +1282,7 @@ def get_connection_object_page(
1251
1282
query_filter = query_filter ,
1252
1283
fetch_size = fetch_size ,
1253
1284
current_cursor = current_cursor ,
1254
- retry_on_auth_error = False ,
1285
+ retry_on_auth_error = True ,
1255
1286
retries_remaining = retries_remaining - 1 ,
1256
1287
)
1257
1288
@@ -1339,7 +1370,7 @@ def get_connection_object_page(
1339
1370
query_filter = query_filter ,
1340
1371
fetch_size = fetch_size ,
1341
1372
current_cursor = current_cursor ,
1342
- retry_on_auth_error = False ,
1373
+ retry_on_auth_error = True ,
1343
1374
retries_remaining = retries_remaining ,
1344
1375
)
1345
1376
raise RuntimeError (f"Query { connection_type } error: { errors } " )
@@ -1377,6 +1408,7 @@ def get_connection_objects(
1377
1408
while has_next_page :
1378
1409
filter_ : str = make_filter (filter_page )
1379
1410
1411
+ self .report .num_expected_tableau_metadata_queries += 1
1380
1412
(
1381
1413
connection_objects ,
1382
1414
current_cursor ,
0 commit comments