├── src ├── requirements.txt ├── config.py ├── lambdainit.py ├── lambdalogging.py ├── backoff.py └── replay.py ├── SQS_replay.png ├── doc └── SQS_replay.png ├── test ├── env.json ├── unit │ ├── conftest.py │ ├── test_constants.py │ └── test_replay.py └── event_sqs_messages.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── template.yml └── CONTRIBUTING.md /src/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | aws-xray-sdk 3 | -------------------------------------------------------------------------------- /SQS_replay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-sqs-dlq-replay-backoff/HEAD/SQS_replay.png -------------------------------------------------------------------------------- /doc/SQS_replay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-sqs-dlq-replay-backoff/HEAD/doc/SQS_replay.png -------------------------------------------------------------------------------- /test/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "ReplayFunction": { 3 | "LOG_LEVEL": "DEBUG", 4 | "SQS_MAIN_URL": "https://sqs.eu-west-1.amazonaws.com/862440218923/sqs-dlq-replay-MainQeue-2D58PFOE4XIV", 5 | "MAX_ATTEMPS": 5, 6 | "BACKOFF_RATE": 2, 7 | "MESSAGE_RETENTION_PERIOD": 200 8 | } 9 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /test/unit/conftest.py: -------------------------------------------------------------------------------- 1 | """Setup unit test environment.""" 2 | 3 | import sys 4 | import os 5 | 6 | import test_constants 7 | 8 | # make sure tests can import the app code 9 | my_path = os.path.dirname(os.path.abspath(__file__)) 10 | sys.path.insert(0, my_path + '/../../src/') 11 | 12 | # set expected config environment variables to test constants -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | """Environment configuration values used by lambda functions.""" 2 | 3 | import os 4 | 5 | LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') 6 | SQS_MAIN_URL = os.getenv('SQS_MAIN_URL') 7 | MAX_ATTEMPS = int(os.getenv('MAX_ATTEMPS')) 8 | BACKOFF_RATE = int(os.getenv('BACKOFF_RATE')) 9 | MESSAGE_RETENTION_PERIOD = int(os.getenv('MESSAGE_RETENTION_PERIOD')) 10 | -------------------------------------------------------------------------------- /test/unit/test_constants.py: -------------------------------------------------------------------------------- 1 | """Constants used for unit tests. 2 | 3 | This can be used to define values for environment variables so unit tests can use these to assert on expected values. 4 | """ 5 | 6 | import os 7 | 8 | os.environ["LOG_LEVEL"] = 'DEBUG' 9 | os.environ["SQS_MAIN_URL"] = 'https://sqs.mock.mock' 10 | os.environ["INTERVAL_SECONDS"] = '2' 11 | os.environ["MAX_ATTEMPS"] = '3' 12 | os.environ["BACKOFF_RATE"] = '2' 13 | os.environ["MESSAGE_RETENTION_PERIOD"] = '1000' 14 | -------------------------------------------------------------------------------- /src/lambdainit.py: -------------------------------------------------------------------------------- 1 | """Special initializations for Lambda functions. 2 | 3 | This file must be imported as the first import in any file containing 4 | a Lambda function handler method. 5 | """ 6 | 7 | import sys 8 | 9 | # add packaged dependencies to search path 10 | sys.path.append('lib') 11 | 12 | # imports of library dependencies must come after setting up 13 | # the dependency search path from aws_xray_sdk.core import patch_all 14 | # noqa: E402 15 | 16 | # patch all supported libraries for X-Ray tracing 17 | # patch_all() 18 | -------------------------------------------------------------------------------- /src/lambdalogging.py: -------------------------------------------------------------------------------- 1 | """Lambda logging helper. 2 | 3 | Returns a Logger with log level set based on env variables. 4 | """ 5 | 6 | import logging 7 | 8 | import config 9 | 10 | # translate log level from string to numeric value 11 | LOG_LEVEL = logging.INFO 12 | 13 | if hasattr(logging, config.LOG_LEVEL): 14 | LOG_LEVEL = getattr(logging, config.LOG_LEVEL) 15 | 16 | def getLogger(name): 17 | """Return a logger configured based on env variables.""" 18 | logger = logging.getLogger(name) 19 | # in lambda environment, logging config has already been setup 20 | # so can't use logging.basicConfig to change log level 21 | logger.setLevel(LOG_LEVEL) 22 | return logger 23 | -------------------------------------------------------------------------------- /src/backoff.py: -------------------------------------------------------------------------------- 1 | """Backoff classes coming from 2 | https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/.""" 3 | import random 4 | 5 | 6 | class Backoff: 7 | """Full Jitter Backoff implementation.""" 8 | 9 | def __init__(self, base, cap): 10 | """Init.""" 11 | self.base = base 12 | self.cap = cap 13 | 14 | def expo(self, n): 15 | """Backoff function.""" 16 | return min(self.cap, pow(2, n) * self.base) 17 | 18 | 19 | class ExpoBackoffFullJitter(Backoff): 20 | """Full Jitter Backoff implementation.""" 21 | 22 | def backoff(self, n): 23 | """Full jitter backoff function.""" 24 | base = self.expo(n) 25 | fulljitter = random.uniform(0, base) 26 | # print("Backoff: %s - Full Jitter Backoff: %s" % (base, fulljitter)) 27 | return fulljitter 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /test/unit/test_replay.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import replay 3 | import os 4 | import json 5 | 6 | 7 | @pytest.fixture 8 | def mock_sqs(mocker): 9 | mocker.patch.object(replay, 'SQS') 10 | return replay.SQS 11 | 12 | 13 | def test_handler(mocker, mock_sqs): 14 | full_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 15 | file = open(full_path + "/event_sqs_messages.json","r") 16 | event = file.read() 17 | event = json.loads(event) 18 | replay.handler(event, None) 19 | args, kwargs = mock_sqs.send_message.call_args 20 | print(kwargs) 21 | 22 | # test sqs message body is not modified 23 | assert "Hello world" == kwargs['MessageBody'] 24 | # test backoff : delay seconds < message retention period 25 | assert int(kwargs['DelaySeconds']) < int(os.environ["MESSAGE_RETENTION_PERIOD"]) 26 | # test sqs attributes is not modified 27 | assert set(('attr1', 'attr2')).issubset(kwargs['MessageAttributes']) 28 | # test number of replay : increment of sqs-dlq-replay-nb 29 | assert int(kwargs['MessageAttributes']['sqs-dlq-replay-nb']['StringValue']) == 2 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQS dead letter queue replay with backoff and jitter 2 | 3 | The theory behind the implementation is described in this article: [Exponential backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) 4 | 5 | This serverless application deploys an AWS lambda function that replays each message of the specified DLQ with an exponential backoff and jitter. After some unseccessful retries the function throws an error which can move the message to a secondary DLQ. 6 | 7 | ## App Architecture 8 | 9 | ![Architecture diagram](SQS_replay.png) 10 | 11 | ## Installation Instructions 12 | 13 | **Deploying the application with the Serverless Application Repository** 14 | 15 | 1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one 16 | 1. Go to the app's page on the sqs-dlq-replay-backoff Serverless Application Repository page and click "Deploy" 17 | 1. Provide the required application parameters and click "Deploy" 18 | 19 | **Deploying the application leveraging SAM (AWS Serverless Application Model):** 20 | 21 | 1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one 22 | 1. Make sure to have [sam cli](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) installed. 23 | 1. Go to the directory you just cloned 24 | 1. Do a `sam build --use-container` (you need to have docker started) 25 | 1. Do a `sam --deploy guided` (it will help you go through the different steps and also parameters available in the stack to configure the jitter and backoff) 26 | 27 | 28 | ## Deployment Outputs 29 | 30 | 1. `ReplayFunctionArn` - My ARN for the Lambda function if needed to be used externally. 31 | 32 | ## Security 33 | 34 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 35 | 36 | ## License 37 | 38 | This library is licensed under the MIT-0 License. See the LICENSE file. 39 | -------------------------------------------------------------------------------- /test/event_sqs_messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "2ac3d0a8-1815-4ed7-aad7-bd5befe90700", 5 | "receiptHandle": "AQEBJIGbQiRCtXfdtinzgXeK2Bd6Lz37Tj8d6gEof9ESAoMEOVfgkRPFbMdFe8mWHeIdrLCP1uEHPC/t1lRK49Ikb3KMllOVQlVnoLRa+f6NvgGlp7SfLJtqa4uo0dJTEO2SN6bZKfX6laUDc9H448ArIoJq4hqLsJrnANp92xObcmLh9EYfaE9olAAbSH9u/Wp6+7fbEu3pvdJJjbLZIocBIV2VptPVFFNE+ZjVZoMuFqgR1Kn8ce227F3exoqOHZWOtBU+tsQpDkrdX4tcIPyHGxTXBQkCpGd6+WJavCmGPK19DdhF05a2smBjZRBrShO2gFmZxkQg7Z2/Zw9lZwcr51aCfYcWImgbKAury0BnHo32Ffrp7r4X++G1hBAU5TAZ18LJlJmQpaAh9gG61o1kRWy5Ub4R5/KGo5/2swoEhvg=", 6 | "body": "Hello world", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1563293665252", 10 | "SenderId": "AROAIAE6ICDTWSZQPHG4M:gmarchan-Isengard", 11 | "ApproximateFirstReceiveTimestamp": "1563293665351" 12 | }, 13 | "messageAttributes": { 14 | "attr2": { 15 | "stringValue": "2", 16 | "stringListValues": [], 17 | "binaryListValues": [], 18 | "dataType": "Number" 19 | }, 20 | "attr1": { 21 | "stringValue": "Hello world", 22 | "stringListValues": [], 23 | "binaryListValues": [], 24 | "dataType": "String" 25 | }, 26 | "sqs-dlq-replay-nb": { 27 | "stringValue": "1", 28 | "stringListValues": [], 29 | "binaryListValues": [], 30 | "dataType": "Number" 31 | } 32 | 33 | }, 34 | "md5OfMessageAttributes": "d4e7c5408a1c6c8bd99f6cab05469692", 35 | "md5OfBody": "b10a8db164e0754105b7a99be72e3fe5", 36 | "eventSource": "aws:sqs", 37 | "eventSourceARN": "arn:aws:sqs:eu-west-1:862440218923:sqs-dlq-replay-DeadLetterQeue-1R3I1WAA4RRBB", 38 | "awsRegion": "eu-west-1" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | 4 | Metadata: 5 | AWS::ServerlessRepo::Application: 6 | Name: amazon-sqs-dlq-replay-backoff 7 | Description: Using an Amazon SQS dead letter queue as en event source, trigger a lambda to replay messages to the main queue. 8 | Author: pinhel 9 | SpdxLicenseId: MIT-0 10 | LicenseUrl: ./LICENSE 11 | ReadmeUrl: ./README.md 12 | Labels: [sqs, dlq, replay, queue, backoff] 13 | HomePageUrl: https://github.com/aws-samples/amazon-sqs-dlq-replay-backoff 14 | SemanticVersion: 3.0.0 15 | SourceCodeUrl: https://github.com/aws-samples/amazon-sqs-dlq-replay-backoff 16 | 17 | AWS::CloudFormation::Interface: 18 | ParameterGroups: 19 | - Label: 20 | default: "SQS Parameters" 21 | Parameters: 22 | - MessageRetentionPeriod 23 | - MainQueueURL 24 | - MainQueueName 25 | - DLQArn 26 | - Label: 27 | default: "Lambda Parameters" 28 | Parameters: 29 | - LogLevel 30 | - BackoffRate 31 | - MaxAttempts 32 | 33 | Parameters: 34 | MessageRetentionPeriod: 35 | Description: 'The number of seconds that Amazon SQS retains a message. You can 36 | specify an integer value from 60 seconds (1 minute) to 1209600 seconds (14 days). This is used in the backoff calculation.' 37 | Type: Number 38 | Default: 345600 39 | MainQueueURL: 40 | Type: String 41 | Description: The URL of the main queue to which messages will be replayed. 42 | MainQueueName: 43 | Type: String 44 | Description: The Name of the main queue to which messages will be replayed. 45 | DLQArn: 46 | Type: String 47 | Description: The ARN of the DLQ from which messages will be replayed. 48 | LogLevel: 49 | Type: String 50 | Description: Log level for Lambda function logging, e.g., ERROR, INFO, DEBUG, etc 51 | Default: DEBUG 52 | MaxAttempts: 53 | Description: An integer, representing the maximum number of replay attempts . If the error recurs more times than specified, retries cease. A value of 0 (zero) is permitted and indicates that the error or errors should never be retried. 54 | Type: Number 55 | Default: 3 56 | BackoffRate: 57 | Description: An integer that is the multiplier by which the replay interval increases on each attempt 58 | Type: Number 59 | Default: 2 60 | 61 | Resources: 62 | ReplayFunction: 63 | Type: AWS::Serverless::Function 64 | Properties: 65 | CodeUri: src/ 66 | Handler: replay.handler 67 | Runtime: python3.11 68 | Tracing: Active 69 | Policies: 70 | - SQSSendMessagePolicy: 71 | QueueName: !Ref MainQueueName 72 | Events: 73 | MySQSEvent: 74 | Type: SQS 75 | Properties: 76 | Queue: !Ref DLQArn 77 | BatchSize: 1 78 | Environment: 79 | Variables: 80 | LOG_LEVEL: !Ref LogLevel 81 | SQS_MAIN_URL: !Ref MainQueueURL 82 | MAX_ATTEMPS: !Ref MaxAttempts 83 | BACKOFF_RATE: !Ref BackoffRate 84 | MESSAGE_RETENTION_PERIOD: !Ref MessageRetentionPeriod 85 | 86 | Outputs: 87 | ReplayFunctionArn: 88 | Description: "Lambda Function ARN" 89 | Value: !GetAtt ReplayFunction.Arn 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/aws-sam-codepipeline-cd/issues), or [recently closed](https://github.com/awslabs/aws-sam-codepipeline-cd/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-sam-codepipeline-cd/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/aws-sam-codepipeline-cd/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /src/replay.py: -------------------------------------------------------------------------------- 1 | """Lambda function handler.""" 2 | 3 | # must be the first import in files with lambda function handlers 4 | import lambdainit # noqa: F401 5 | 6 | import lambdalogging 7 | import boto3 8 | import config 9 | import backoff 10 | 11 | LOG = lambdalogging.getLogger(__name__) 12 | SQS = boto3.client('sqs') 13 | 14 | 15 | def handler(event, context): 16 | """Lambda function handler.""" 17 | LOG.info('Received event: %s', event) 18 | LOG.debug('Main SQS queue ARN: %s', config.SQS_MAIN_URL) 19 | LOG.debug('Max attemps: %s', config.MAX_ATTEMPS) 20 | LOG.debug('Backoff rate: %s', config.BACKOFF_RATE) 21 | LOG.debug('Message retention period: %s', config.MESSAGE_RETENTION_PERIOD) 22 | 23 | for record in event['Records']: 24 | nbReplay = 0 25 | # number of replay 26 | if 'sqs-dlq-replay-nb' in record['messageAttributes']: 27 | nbReplay = int(record['messageAttributes']['sqs-dlq-replay-nb']["stringValue"]) 28 | 29 | LOG.info('Number of retries already done: %s', nbReplay) 30 | nbReplay += 1 31 | if nbReplay > config.MAX_ATTEMPS: 32 | raise MaxAttempsError(replay=nbReplay, max=config.MAX_ATTEMPS) 33 | 34 | # SQS attributes 35 | attributes = record['messageAttributes'] 36 | attributes.update( 37 | {'sqs-dlq-replay-nb': {'StringValue': str(nbReplay), 'DataType': 'Number'}}) 38 | 39 | LOG.debug("SQS message attributes: %s", attributes) 40 | _sqs_attributes_cleaner(attributes) 41 | LOG.debug("SQS message attributes cleaned: %s", attributes) 42 | 43 | # Backoff 44 | b = backoff.ExpoBackoffFullJitter( 45 | base=config.BACKOFF_RATE, 46 | cap=config.MESSAGE_RETENTION_PERIOD) 47 | delaySeconds = b.backoff(n=int(nbReplay)) 48 | 49 | # If delaySeconds is greater than 900 (SQS limit), put it to 900 50 | if int(delaySeconds) > 900: 51 | delaySeconds = 900 52 | 53 | # SQS 54 | msgreplay = "Message replayed to main SQS queue with delayseconds" 55 | LOG.info(msgreplay + "%s", delaySeconds) 56 | 57 | #check for FIFO SQS 58 | if config.SQS_MAIN_URL.endswith('.fifo'): 59 | if 'MessageGroupId' in record['attributes']: 60 | SQS.send_message( 61 | QueueUrl=config.SQS_MAIN_URL, 62 | MessageBody=record['body'], 63 | MessageAttributes=record['messageAttributes'], 64 | MessageGroupId=record['attributes']['MessageGroupId'], 65 | MessageDeduplicationId=record['attributes']['MessageDeduplicationId'] 66 | ) 67 | else: 68 | SQS.send_message( 69 | QueueUrl=config.SQS_MAIN_URL, 70 | MessageBody=record['body'], 71 | DelaySeconds=int(delaySeconds), 72 | MessageAttributes=record['messageAttributes'] 73 | ) 74 | 75 | 76 | def _sqs_attributes_cleaner(attributes): 77 | """Transform SQS attributes from Lambda event to SQS message.""" 78 | d = dict.fromkeys(attributes) 79 | for k in d: 80 | if isinstance(attributes[k], dict): 81 | subd = dict.fromkeys(attributes[k]) 82 | for subk in subd: 83 | if not attributes[k][subk]: 84 | del attributes[k][subk] 85 | else: 86 | attributes[k][''.join(subk[:1].upper() + subk[1:])] = attributes[k].pop(subk) 87 | 88 | 89 | class MaxAttempsError(Exception): 90 | """Raised when the max attempts is reached.""" 91 | 92 | def __init__(self, replay, max, msg=None): 93 | """Init.""" 94 | if msg is None: 95 | msg = "An error occured : " 96 | "Number of retries(%s) is sup max attemps(%s)" % (replay, max) 97 | super(MaxAttempsError, self).__init__(msg) 98 | self.replay = replay 99 | self.max = max 100 | --------------------------------------------------------------------------------