diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d33d157..9dfebbca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Removed duplicate import of transaction_pb2 in transaction.py ### Fixed + +- Fixed `scripts/examples/match_examples_src.py` to correctly match examples with exact filenames in different folders. - fixed workflow: changelog check with improved sensitivity to deletions, additions, new releases ## [0.1.9] - 2025-11-26 @@ -58,6 +60,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added `docs\sdk_developers\training\receipts.md` as a training guide for users to understand hedera receipts. - Add `set_token_ids`, `_from_proto`, `_validate_checksum` to TokenAssociateTransaction [#795] - docs: added `network_and_client.md` with a table of contents, and added external example scripts (`client.py`). +- Add comprehensive documentation for `MaxAttemptsError` in `docs/sdk_developers/training/max_attempts_error.md` (2025-11-26) +- Add practical example `examples/errors/max_attempts_error.py` demonstrating network error handling and recovery strategies (2025-11-26) +- Document error handling patterns for network failures and node retry attempts (#877) ### Changed @@ -74,6 +79,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - changed to add concurrency to workflow bot - feat: Refactor `TokenDissociateTransaction` to use set_token_ids method and update transaction fee to Hbar, also update `transaction.py` and expand `examples/token_dissociate.py`, `tests/unit/token_dissociate.py`. + ### Fixed - chore: updated solo action to avoid v5 diff --git a/docs/sdk_developers/training/max_attempts_error.md b/docs/sdk_developers/training/max_attempts_error.md new file mode 100644 index 000000000..15bb9d623 --- /dev/null +++ b/docs/sdk_developers/training/max_attempts_error.md @@ -0,0 +1,123 @@ +# MaxAttemptsError + +## Overview + +`MaxAttemptsError` is an exception that occurs when the SDK has exhausted all retry attempts to communicate with a Hedera network node. This error represents transient network issues or node unavailability rather than transaction logic errors. + +## When It Occurs + +`MaxAttemptsError` is raised when: +- A node repeatedly fails to respond to requests +- Network connectivity issues prevent communication with a node +- The node is temporarily unavailable or overloaded +- Multiple retry attempts have all failed + +## Error Attributes + +The `MaxAttemptsError` exception provides the following attributes: + +- **`message`** (str): The error message explaining why the maximum attempts were reached +- **`node_id`** (str): The ID of the node that was being contacted when the max attempts were reached +- **`last_error`** (Exception): The last error that occurred during the final attempt + +## Error Handling Context + +Understanding the different stages of failure is crucial for proper error handling: + +1. **Precheck Errors** (`PrecheckError`): Failures before transaction submission (e.g., insufficient balance, invalid signature) +2. **MaxAttemptsError**: Network/node retry failures during communication +3. **Receipt Status Errors** (`ReceiptStatusError`): Failures after consensus (e.g., smart contract revert) + +Many developers assume that if `execute()` doesn't throw, the transaction succeeded. These exceptions explicitly show different stages of failure. + +## Example Usage + +```python +from hiero_sdk_python import Client, TransferTransaction +from hiero_sdk_python.exceptions import MaxAttemptsError, PrecheckError, ReceiptStatusError + +# Create client and transaction +client = Client.forTestnet() +transaction = TransferTransaction() + .addHbarTransfer(sender_account, -1000) + .addHbarTransfer(receiver_account, 1000) + +try: + # Execute the transaction + receipt = transaction.execute(client) + print("Transaction executed successfully") + +except PrecheckError as e: + # Handle precheck failures (before submission) + print(f"Precheck failed: {e.message}") + print(f"Status: {e.status}") + +except MaxAttemptsError as e: + # Handle network/node retry failures + print(f"Max attempts reached on node {e.node_id}: {e.message}") + if e.last_error: + print(f"Last error: {e.last_error}") + + # Common recovery strategies: + # 1. Retry with a different node + # 2. Wait and retry the same node + # 3. Check network connectivity + +except ReceiptStatusError as e: + # Handle post-consensus failures + print(f"Transaction failed after consensus: {e.message}") + print(f"Status: {e.status}") +``` + +## Recovery Strategies + +When encountering a `MaxAttemptsError`, consider these approaches: + +### 1. Retry with Different Node +```python +try: + receipt = transaction.execute(client) +except MaxAttemptsError as e: + # Switch to a different node and retry + client.setNetwork([{"node2.hedera.com:50211": "0.0.2"}]) + receipt = transaction.execute(client) +``` + +### 2. Exponential Backoff +```python +import time + +def execute_with_retry(transaction, client, max_retries=3): + for attempt in range(max_retries): + try: + return transaction.execute(client) + except MaxAttemptsError as e: + if attempt == max_retries - 1: + raise + wait_time = 2 ** attempt # 1, 2, 4 seconds + time.sleep(wait_time) +``` + +### 3. Node Health Check +```python +try: + receipt = transaction.execute(client) +except MaxAttemptsError as e: + print(f"Node {e.node_id} appears unhealthy") + # Implement node health monitoring + # Consider removing the node from rotation temporarily +``` + +## Best Practices + +1. **Always catch MaxAttemptsError separately** from other exceptions to implement appropriate retry logic +2. **Log the node_id** to identify problematic nodes in your network +3. **Implement circuit breakers** to temporarily skip consistently failing nodes +4. **Use exponential backoff** when retrying to avoid overwhelming the network +5. **Monitor last_error** to understand the root cause of failures + +## Related Documentation + +- [Receipt Status Error](receipt_status_error.md) - Understanding post-consensus failures +- [Receipts](receipts.md) - Working with transaction receipts +- [Error Handling Guide](../common_issues.md) - General error handling strategies diff --git a/examples/errors/max_attempts_error.py b/examples/errors/max_attempts_error.py new file mode 100644 index 000000000..9faab6f33 --- /dev/null +++ b/examples/errors/max_attempts_error.py @@ -0,0 +1,285 @@ +""" +Example demonstrating MaxAttemptsError handling in Hedera SDK + +This example shows how to handle MaxAttemptsError exceptions that occur +when the SDK exhausts retry attempts to communicate with network nodes. +""" + +import time +from hiero_sdk_python import Client, TransferTransaction, AccountId, PrivateKey +from hiero_sdk_python.exceptions import MaxAttemptsError, PrecheckError, ReceiptStatusError + + +def basic_max_attempts_example(): + """ + Basic example of catching MaxAttemptsError + """ + print("=== Basic MaxAttemptsError Example ===") + + # Initialize client + client = Client.forTestnet() + + # Create a simple transfer transaction + sender = AccountId.fromString("0.0.12345") + receiver = AccountId.fromString("0.0.67890") + private_key = PrivateKey.fromString("302e020100300506032b657004220420db484b8284a5c6826c0e07c2d8296fda0b841d4e1dc4b7f308a2db69b043a8c5") + + transaction = TransferTransaction() + .addHbarTransfer(sender, -1000) + .addHbarTransfer(receiver, 1000) + .freezeWith(client) + .sign(private_key) + + try: + receipt = transaction.execute(client) + print("āœ… Transaction executed successfully") + + except PrecheckError as e: + print(f"āŒ Precheck failed: {e.message}") + print(f" Status: {e.status}") + + except MaxAttemptsError as e: + print(f"āŒ Max attempts reached on node {e.node_id}") + print(f" Error: {e.message}") + if e.last_error: + print(f" Last error: {type(e.last_error).__name__}: {e.last_error}") + + # Example recovery action + print(" šŸ’” Recovery: Consider retrying with a different node") + + except ReceiptStatusError as e: + print(f"āŒ Transaction failed after consensus: {e.message}") + print(f" Status: {e.status}") + + except Exception as e: + print(f"āŒ Unexpected error: {type(e).__name__}: {e}") + + +def retry_with_different_node(): + """ + Example demonstrating retry with a different node when MaxAttemptsError occurs + """ + print("\n=== Retry with Different Node Example ===") + + # Initialize client with multiple nodes + client = Client.forTestnet() + + # Define alternative nodes + alternative_nodes = [ + {"0.0.3": "35.237.200.180:50211"}, + {"0.0.4": "35.186.191.247:50211"}, + {"0.0.5": "35.192.2.44:50211"} + ] + + sender = AccountId.fromString("0.0.12345") + receiver = AccountId.fromString("0.0.67890") + private_key = PrivateKey.fromString("302e020100300506032b657004220420db484b8284a5c6826c0e07c2d8296fda0b841d4e1dc4b7f308a2db69b043a8c5") + + transaction = TransferTransaction() + .addHbarTransfer(sender, -1000) + .addHbarTransfer(receiver, 1000) + .freezeWith(client) + .sign(private_key) + + # Try with original node first + try: + receipt = transaction.execute(client) + print("āœ… Transaction executed successfully with original node") + return receipt + + except MaxAttemptsError as e: + print(f"āš ļø Original node {e.node_id} failed after max attempts") + print(f" Error: {e.message}") + + # Try alternative nodes + for i, node_map in enumerate(alternative_nodes): + try: + print(f" šŸ”„ Trying alternative node {i+1}...") + client.setNetwork([node_map]) + receipt = transaction.execute(client) + print(f"āœ… Transaction succeeded with alternative node {i+1}") + return receipt + + except MaxAttemptsError as retry_error: + print(f" āŒ Alternative node {i+1} also failed: {retry_error.message}") + continue + + print("āŒ All nodes failed. Transaction could not be completed.") + return None + + +def exponential_backoff_retry(): + """ + Example demonstrating exponential backoff retry strategy + """ + print("\n=== Exponential Backoff Retry Example ===") + + client = Client.forTestnet() + + sender = AccountId.fromString("0.0.12345") + receiver = AccountId.fromString("0.0.67890") + private_key = PrivateKey.fromString("302e020100300506032b657004220420db484b8284a5c6826c0e07c2d8296fda0b841d4e1dc4b7f308a2db69b043a8c5") + + transaction = TransferTransaction() + .addHbarTransfer(sender, -1000) + .addHbarTransfer(receiver, 1000) + .freezeWith(client) + .sign(private_key) + + max_retries = 3 + + for attempt in range(max_retries): + try: + print(f" šŸ“¤ Attempt {attempt + 1} of {max_retries}...") + receipt = transaction.execute(client) + print(f"āœ… Transaction succeeded on attempt {attempt + 1}") + return receipt + + except MaxAttemptsError as e: + if attempt == max_retries - 1: + print(f"āŒ All {max_retries} attempts failed") + print(f" Final error: {e.message}") + return None + + # Calculate wait time: 1s, 2s, 4s for attempts 0, 1, 2 + wait_time = 2 ** attempt + print(f" ā³ Waiting {wait_time} seconds before retry...") + print(f" Last error: {e.last_error if e.last_error else 'No specific error'}") + time.sleep(wait_time) + + except Exception as e: + print(f"āŒ Unexpected error on attempt {attempt + 1}: {type(e).__name__}: {e}") + break + + return None + + +def node_health_monitoring(): + """ + Example showing how to monitor node health based on MaxAttemptsError + """ + print("\n=== Node Health Monitoring Example ===") + + # Simple node health tracking + node_failures = {} + + client = Client.forTestnet() + + sender = AccountId.fromString("0.0.12345") + receiver = AccountId.fromString("0.0.67890") + private_key = PrivateKey.fromString("302e020100300506032b657004220420db484b8284a5c6826c0e07c2d8296fda0b841d4e1dc4b7f308a2db69b043a8c5") + + transaction = TransferTransaction() + .addHbarTransfer(sender, -1000) + .addHbarTransfer(receiver, 1000) + .freezeWith(client) + .sign(private_key) + + try: + receipt = transaction.execute(client) + print("āœ… Transaction executed successfully") + + except MaxAttemptsError as e: + node_id = e.node_id + error_msg = e.message + + # Track node failures + if node_id not in node_failures: + node_failures[node_id] = [] + node_failures[node_id].append({ + 'timestamp': time.time(), + 'error': error_msg, + 'last_error': str(e.last_error) if e.last_error else None + }) + + print(f"āŒ Node {node_id} failure recorded") + print(f" Error: {error_msg}") + print(f" Total failures for this node: {len(node_failures[node_id])}") + + # Simple health check logic + if len(node_failures[node_id]) >= 3: + print(f" 🚨 Node {node_id} marked as unhealthy (3+ failures)") + print(" šŸ’” Consider removing this node from rotation temporarily") + + except Exception as e: + print(f"āŒ Unexpected error: {type(e).__name__}: {e}") + + # Print node health summary + if node_failures: + print("\nšŸ“Š Node Health Summary:") + for node_id, failures in node_failures.items(): + print(f" Node {node_id}: {len(failures)} failures") + + +def comprehensive_error_handling(): + """ + Comprehensive example showing all error types and their handling + """ + print("\n=== Comprehensive Error Handling Example ===") + + client = Client.forTestnet() + + sender = AccountId.fromString("0.0.12345") + receiver = AccountId.fromString("0.0.67890") + private_key = PrivateKey.fromString("302e020100300506032b657004220420db484b8284a5c6826c0e07c2d8296fda0b841d4e1dc4b7f308a2db69b043a8c5") + + transaction = TransferTransaction() + .addHbarTransfer(sender, -1000) + .addHbarTransfer(receiver, 1000) + .freezeWith(client) + .sign(private_key) + + try: + receipt = transaction.execute(client) + print("āœ… Transaction executed successfully") + print(f" Transaction ID: {receipt.transactionId}") + + except PrecheckError as e: + print("šŸ” PRECHECK ERROR (Before submission)") + print(f" Status: {e.status}") + print(f" Message: {e.message}") + if e.transaction_id: + print(f" Transaction ID: {e.transaction_id}") + print(" šŸ’” Fix: Check account balance, fees, signatures, etc.") + + except MaxAttemptsError as e: + print("🌐 MAX ATTEMPTS ERROR (Network/Node failure)") + print(f" Node ID: {e.node_id}") + print(f" Message: {e.message}") + if e.last_error: + print(f" Last error: {type(e.last_error).__name__}: {e.last_error}") + print(" šŸ’” Fix: Retry with different node, check network, wait and retry") + + except ReceiptStatusError as e: + print("šŸ“‹ RECEIPT STATUS ERROR (After consensus)") + print(f" Status: {e.status}") + print(f" Transaction ID: {e.transaction_id}") + print(f" Message: {e.message}") + print(" šŸ’” Fix: Check smart contract logic, account state, etc.") + + except Exception as e: + print("āŒ UNEXPECTED ERROR") + print(f" Type: {type(e).__name__}") + print(f" Message: {e}") + print(" šŸ’” Fix: Check SDK version, network configuration, etc.") + + +if __name__ == "__main__": + print("MaxAttemptsError Handling Examples") + print("=" * 50) + + # Run all examples + basic_max_attempts_example() + retry_with_different_node() + exponential_backoff_retry() + node_health_monitoring() + comprehensive_error_handling() + + print("\n" + "=" * 50) + print("Examples completed!") + print("\nKey Takeaways:") + print("• MaxAttemptsError indicates network/node issues, not transaction logic errors") + print("• Always catch MaxAttemptsError separately for proper retry logic") + print("• Log node_id to identify problematic nodes") + print("• Implement exponential backoff when retrying") + print("• Consider circuit breakers for consistently failing nodes")