3232
3333
3434class CommandType (Enum ):
35- EXECUTE_STATEMENT = "ExecuteStatement"
35+ NOT_SET = "NotSet"
36+ OPEN_SESSION = "OpenSession"
3637 CLOSE_SESSION = "CloseSession"
38+ METADATA = "Metadata"
3739 CLOSE_OPERATION = "CloseOperation"
38- GET_OPERATION_STATUS = "GetOperationStatus"
40+ CANCEL_OPERATION = "CancelOperation"
41+ EXECUTE_STATEMENT = "ExecuteStatement"
42+ FETCH_RESULTS = "FetchResults"
43+ CLOUD_FETCH = "CloudFetch"
44+ AUTH = "Auth"
45+ TELEMETRY_PUSH = "TelemetryPush"
46+ VOLUME_GET = "VolumeGet"
47+ VOLUME_PUT = "VolumePut"
48+ VOLUME_DELETE = "VolumeDelete"
3949 OTHER = "Other"
4050
4151 @classmethod
@@ -45,9 +55,66 @@ def get(cls, value: str):
4555 if valid_command :
4656 return getattr (cls , str (valid_command ))
4757 else :
58+ # Map Thrift metadata operations to METADATA type
59+ metadata_operations = {
60+ "GetOperationStatus" , "GetResultSetMetadata" , "GetTables" ,
61+ "GetColumns" , "GetSchemas" , "GetCatalogs" , "GetFunctions" ,
62+ "GetPrimaryKeys" , "GetTypeInfo" , "GetCrossReference" ,
63+ "GetImportedKeys" , "GetExportedKeys" , "GetTableTypes"
64+ }
65+ if value in metadata_operations :
66+ return cls .METADATA
4867 return cls .OTHER
4968
5069
70+ class CommandIdempotency (Enum ):
71+ IDEMPOTENT = "idempotent"
72+ NON_IDEMPOTENT = "non_idempotent"
73+
74+
75+ # Mapping of CommandType to CommandIdempotency
76+ # Based on the official idempotency classification
77+ COMMAND_IDEMPOTENCY_MAP = {
78+ # NON-IDEMPOTENT operations (safety first - unknown types are not retried)
79+ CommandType .NOT_SET : CommandIdempotency .NON_IDEMPOTENT ,
80+ CommandType .EXECUTE_STATEMENT : CommandIdempotency .NON_IDEMPOTENT ,
81+ CommandType .FETCH_RESULTS : CommandIdempotency .NON_IDEMPOTENT ,
82+ CommandType .VOLUME_PUT : CommandIdempotency .NON_IDEMPOTENT , # PUT can overwrite files
83+
84+ # IDEMPOTENT operations
85+ CommandType .OPEN_SESSION : CommandIdempotency .IDEMPOTENT ,
86+ CommandType .CLOSE_SESSION : CommandIdempotency .IDEMPOTENT ,
87+ CommandType .METADATA : CommandIdempotency .IDEMPOTENT ,
88+ CommandType .CLOSE_OPERATION : CommandIdempotency .IDEMPOTENT ,
89+ CommandType .CANCEL_OPERATION : CommandIdempotency .IDEMPOTENT ,
90+ CommandType .CLOUD_FETCH : CommandIdempotency .IDEMPOTENT ,
91+ CommandType .AUTH : CommandIdempotency .IDEMPOTENT ,
92+ CommandType .TELEMETRY_PUSH : CommandIdempotency .IDEMPOTENT ,
93+ CommandType .VOLUME_GET : CommandIdempotency .IDEMPOTENT ,
94+ CommandType .VOLUME_DELETE : CommandIdempotency .IDEMPOTENT ,
95+ CommandType .OTHER : CommandIdempotency .IDEMPOTENT ,
96+ }
97+
98+ # HTTP status codes that should never be retried, even for idempotent requests
99+ # These are client error codes that indicate permanent issues
100+ NON_RETRYABLE_STATUS_CODES = {
101+ 400 , # Bad Request
102+ 401 , # Unauthorized
103+ 403 , # Forbidden
104+ 404 , # Not Found
105+ 405 , # Method Not Allowed
106+ 409 , # Conflict
107+ 410 , # Gone
108+ 411 , # Length Required
109+ 412 , # Precondition Failed
110+ 413 , # Payload Too Large
111+ 414 , # URI Too Long
112+ 415 , # Unsupported Media Type
113+ 416 , # Range Not Satisfiable
114+ 501 , # Not Implemented
115+ }
116+
117+
51118class DatabricksRetryPolicy (Retry ):
52119 """
53120 Implements our v3 retry policy by extending urllib3's robust default retry behaviour.
@@ -354,38 +421,25 @@ def should_retry(self, method: str, status_code: int) -> Tuple[bool, str]:
354421
355422 logger .info (f"Received status code { status_code } for { method } request" )
356423
424+ # Get command idempotency for use in multiple conditions below
425+ command_idempotency = COMMAND_IDEMPOTENCY_MAP .get (
426+ self .command_type , CommandIdempotency .NON_IDEMPOTENT
427+ )
428+
357429 # Request succeeded. Don't retry.
358430 if status_code // 100 <= 3 :
359431 return False , "2xx/3xx codes are not retried"
360432
361- if status_code == 400 :
362- return (
363- False ,
364- "Received 400 - BAD_REQUEST. Please check the request parameters." ,
365- )
366-
367- if status_code == 401 :
368- return (
369- False ,
370- "Received 401 - UNAUTHORIZED. Confirm your authentication credentials." ,
371- )
372-
373- if status_code == 403 :
374- return False , "403 codes are not retried"
375-
376- # Request failed and server said NotImplemented. This isn't recoverable. Don't retry.
377- if status_code == 501 :
378- return False , "Received code 501 from server."
379433
380434 # Request failed and this method is not retryable. We only retry POST requests.
381435 if not self ._is_method_retryable (method ):
382436 return False , "Only POST requests are retried"
383437
384438 # Request failed with 404 and was a GetOperationStatus. This is not recoverable. Don't retry.
385- if status_code == 404 and self .command_type == CommandType .GET_OPERATION_STATUS :
439+ if status_code == 404 and self .command_type == CommandType .METADATA :
386440 return (
387441 False ,
388- "GetOperationStatus received 404 code from Databricks. Operation was canceled." ,
442+ "Metadata request received 404 code from Databricks. Operation was canceled." ,
389443 )
390444
391445 # Request failed with 404 because CloseSession returns 404 if you repeat the request.
@@ -408,23 +462,26 @@ def should_retry(self, method: str, status_code: int) -> Tuple[bool, str]:
408462 "CloseOperation received 404 code from Databricks. Cursor is already closed."
409463 )
410464
465+ if status_code in NON_RETRYABLE_STATUS_CODES :
466+ return False , f"Received { status_code } code from Databricks. Operation was canceled."
467+
411468 # Request failed, was an ExecuteStatement and the command may have reached the server
412469 if (
413- self . command_type == CommandType . EXECUTE_STATEMENT
470+ command_idempotency == CommandIdempotency . NON_IDEMPOTENT
414471 and status_code not in self .status_forcelist
415472 and status_code not in self .force_dangerous_codes
416473 ):
417474 return (
418475 False ,
419- "ExecuteStatement command can only be retried for codes 429 and 503" ,
476+ "Non Idempotent requests can only be retried for codes 429 and 503" ,
420477 )
421478
422479 # Request failed with a dangerous code, was an ExecuteStatement, but user forced retries for this
423480 # dangerous code. Note that these lines _are not required_ to make these requests retry. They would
424481 # retry automatically. This code is included only so that we can log the exact reason for the retry.
425482 # This gives users signal that their _retry_dangerous_codes setting actually did something.
426483 if (
427- self . command_type == CommandType . EXECUTE_STATEMENT
484+ command_idempotency == CommandIdempotency . NON_IDEMPOTENT
428485 and status_code in self .force_dangerous_codes
429486 ):
430487 return (
0 commit comments