@@ -48,6 +48,40 @@ def get(cls, value: str):
4848 return cls .OTHER
4949
5050
51+ class CommandIdempotency (Enum ):
52+ IDEMPOTENT = "idempotent"
53+ NON_IDEMPOTENT = "non_idempotent"
54+
55+
56+ # Mapping of CommandType to CommandIdempotency
57+ # Only EXECUTE_STATEMENT is non-idempotent, all others are idempotent
58+ COMMAND_IDEMPOTENCY_MAP = {
59+ CommandType .EXECUTE_STATEMENT : CommandIdempotency .NON_IDEMPOTENT ,
60+ CommandType .CLOSE_SESSION : CommandIdempotency .IDEMPOTENT ,
61+ CommandType .CLOSE_OPERATION : CommandIdempotency .IDEMPOTENT ,
62+ CommandType .GET_OPERATION_STATUS : CommandIdempotency .IDEMPOTENT ,
63+ CommandType .OTHER : CommandIdempotency .IDEMPOTENT ,
64+ }
65+
66+ # HTTP status codes that should never be retried, even for idempotent requests
67+ # These are client error codes that indicate permanent issues
68+ NON_RETRYABLE_STATUS_CODES = {
69+ 400 , # Bad Request
70+ 401 , # Unauthorized
71+ 403 , # Forbidden
72+ 404 , # Not Found
73+ 405 , # Method Not Allowed
74+ 409 , # Conflict
75+ 410 , # Gone
76+ 411 , # Length Required
77+ 412 , # Precondition Failed
78+ 413 , # Payload Too Large
79+ 414 , # URI Too Long
80+ 415 , # Unsupported Media Type
81+ 416 , # Range Not Satisfiable
82+ }
83+
84+
5185class DatabricksRetryPolicy (Retry ):
5286 """
5387 Implements our v3 retry policy by extending urllib3's robust default retry behaviour.
@@ -358,21 +392,13 @@ def should_retry(self, method: str, status_code: int) -> Tuple[bool, str]:
358392 if status_code // 100 <= 3 :
359393 return False , "2xx/3xx codes are not retried"
360394
361- if status_code == 400 :
395+ # Check for non-retryable status codes that should never be retried
396+ if status_code in NON_RETRYABLE_STATUS_CODES :
362397 return (
363398 False ,
364- "Received 400 - BAD_REQUEST. Please check the request parameters. " ,
399+ f" { self . command_type . value if self . command_type else 'Unknown' } received { status_code } - Client error codes are not retried " ,
365400 )
366401
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-
376402 # Request failed and server said NotImplemented. This isn't recoverable. Don't retry.
377403 if status_code == 501 :
378404 return False , "Received code 501 from server."
@@ -381,50 +407,26 @@ def should_retry(self, method: str, status_code: int) -> Tuple[bool, str]:
381407 if not self ._is_method_retryable (method ):
382408 return False , "Only POST requests are retried"
383409
384- # 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 :
386- return (
387- False ,
388- "GetOperationStatus received 404 code from Databricks. Operation was canceled." ,
389- )
390-
391- # Request failed with 404 because CloseSession returns 404 if you repeat the request.
392- if (
393- status_code == 404
394- and self .command_type == CommandType .CLOSE_SESSION
395- and len (self .history ) > 0
396- ):
397- raise SessionAlreadyClosedError (
398- "CloseSession received 404 code from Databricks. Session is already closed."
399- )
400-
401- # Request failed with 404 because CloseOperation returns 404 if you repeat the request.
402- if (
403- status_code == 404
404- and self .command_type == CommandType .CLOSE_OPERATION
405- and len (self .history ) > 0
406- ):
407- raise CursorAlreadyClosedError (
408- "CloseOperation received 404 code from Databricks. Cursor is already closed."
409- )
410+ # Get command idempotency for use in multiple conditions below
411+ command_idempotency = COMMAND_IDEMPOTENCY_MAP .get (self .command_type , CommandIdempotency .IDEMPOTENT )
410412
411- # Request failed, was an ExecuteStatement and the command may have reached the server
413+ # Request failed, was a non-idempotent command and the command may have reached the server
412414 if (
413- self . command_type == CommandType . EXECUTE_STATEMENT
415+ command_idempotency == CommandIdempotency . NON_IDEMPOTENT
414416 and status_code not in self .status_forcelist
415417 and status_code not in self .force_dangerous_codes
416418 ):
417419 return (
418420 False ,
419- "ExecuteStatement command can only be retried for codes 429 and 503" ,
421+ f" { self . command_type . value } command can only be retried for codes 429 and 503" ,
420422 )
421423
422- # Request failed with a dangerous code, was an ExecuteStatement , but user forced retries for this
424+ # Request failed with a dangerous code, was a non-idempotent command , but user forced retries for this
423425 # dangerous code. Note that these lines _are not required_ to make these requests retry. They would
424426 # retry automatically. This code is included only so that we can log the exact reason for the retry.
425427 # This gives users signal that their _retry_dangerous_codes setting actually did something.
426428 if (
427- self . command_type == CommandType . EXECUTE_STATEMENT
429+ command_idempotency == CommandIdempotency . NON_IDEMPOTENT
428430 and status_code in self .force_dangerous_codes
429431 ):
430432 return (
0 commit comments