Exercise - Implement a retry policy in your app

Completed

Your team's chat app has been improved to detect errors. When you use a cloud-based database, there are different transient errors that can occur. In this exercise, we focus on resolving connection problems by implementing a retry policy.

If the error is due to database connection issues, we adopt the following strategy:

  • If the error is network-related, quickly try to reconnect
  • Keep retrying to reconnect every 60 seconds
  • Retry up to five times
  • After five retries, notify the end user of the database problem and quit

For other unknown errors, quit the app.

Use a class to implement a retry policy

  1. Run the following command in the Cloud Shell to navigate to the C# chatapp-retry folder.

    cd ~/mslearn-handle-transient-errors-in-your-app/csharp/chatapp-retry/
    
  2. This version includes your first draft at coding a retry policy class. It includes an appsettings.json configuration file to allow system administrators to set the delay and number of retries.

    {
        "number-of-retries": 5,
        "delay": 60000
    }
    
  3. Open the RetryPolicy class.

    code RetryPolicy.cs
    

    Take a moment to read through the code in this class.

    The RetryPolicy class keeps track of the number of retries, and handles adding a delay before code can be retried. Most of the logic for our retry policy is in the CanRetry() method:

    using System;
    using Microsoft.Extensions.Configuration;
    using System.Threading;
    using System.Diagnostics;
    using System.IO;
    
    
    namespace csharp_chatapp_retry
    {
        public class RetryPolicy
        {
            private int currentTries = 0;
            static public IConfiguration configuration { get; set; }
    
            /// <summary>
            /// Constructor - reads config for number of retries and delay
            /// </summary>
            public RetryPolicy()
            {
                ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
                configurationBuilder.SetBasePath(Directory.GetCurrentDirectory());
                configurationBuilder.AddJsonFile("appsettings.json");
                configuration = configurationBuilder.Build();
            }
    
            /// <summary>
            /// Method to implement retry policy controlled by configuration
            /// </summary>
            /// <returns>Returns true if it's ok to retry</returns>
            public bool CanRetry()
            {
                // Keep track of current retries
                currentTries++;
                Console.WriteLine($"Retrying: {currentTries}");
    
                // Use a delay if this isn't the first try
                if (currentTries != 1)
                {
                    Thread.Sleep(int.Parse(configuration["delay"]));
                }
    
                if (currentTries < int.Parse(configuration["number-of-retries"])) {
                    return true;
                } else {
                    return false;
                }
            }
    
            public void ResetRetries()
            {
                currentTries = 0;
            }
        }
    }
    

    The canRetry method checks the number of retries, and then returns true to the calling code if it's OK to keep retrying, or false if the app should now stop.

    Now we can use this class in our app to implement a retry policy.

Add a retry policy

  1. Select the three ellipses (...) to the top right of the editor and then select Open File .... Select Program.cs in the file chooser dialog, and press Enter.

  2. Update the connectionString variable in this class to the value of the connection string of your Azure Cosmos DB database we found earlier.

  3. Scroll down to the getAllChats() method.

    private static void getAllChats()
    {
        messages = database.GetCollection<ChatMessage>(collectionName);
        try
        {
            allMessages = messages.Find(new BsonDocument()).ToList();
            foreach (ChatMessage chat in allMessages)
            {
                Console.WriteLine($"{chat.Name}: {chat.Message}");
            }
            Console.WriteLine("\n");
        }
        catch (MongoDB.Driver.MongoConnectionException e)
        {
            diagnose(e);
        }
        catch (System.TimeoutException e)
        {
            diagnose(e);
        }
        catch (Exception e)
        {
            diagnose(e);
            throw e;
        }
    }
    
  4. Add the code to retry the connection in the two MongoDB-specific catch blocks.

        if (retries.CanRetry())
        {
            diagnose(e);
            getAllChats(); //retry
        } else {
            Console.WriteLine("Maximum retries - need to close.");
            throw e;
        }
    
  5. The method should be the following code:

        private static void getAllChats()
        {
            messages = database.GetCollection<ChatMessage>(collectionName);
            try
            {
                allMessages = messages.Find(new BsonDocument()).ToList();
                foreach (ChatMessage chat in allMessages)
                {
                    Console.WriteLine(String.Format("{0}: {1}", chat.Name, chat.Message));
                }
                Console.WriteLine("\n");
            }
            catch (MongoDB.Driver.MongoConnectionException e)
            {
                if (retries.CanRetry())
                {
                    diagnose(e);
                    getAllChats(); //retry
                } else {
                    Console.WriteLine("Maximum retries - need to close.");
                    throw e;
                }
            }
            catch (System.TimeoutException e)
            {
                if (retries.CanRetry())
                {
                    diagnose(e);
                    getAllChats(); //retry
                } else {
                    Console.WriteLine("Maximum retries - need to close.");
                    throw e;
                }
            }
            catch (Exception e)
            {
                diagnose(e);
                throw e;
            }
        }
    
  6. Define the retries object, and reset it with each call to the database. Add a declaration for the retries object before the main method:

        private static RetryPolicy retries = new RetryPolicy();
    
  7. Reset the retries in the while loop of the Main() method in Program.cs, as shown in the following code snippet.

    while(choice != 'Q' && choice != 'q')
    {
        retries.Reset();
        Console.WriteLine();
        ChatMessage newChat = new ChatMessage();
        switch (choice)
        {
            case 'N':
    
  8. To save our changes, select the three ellipses (...) to the top right of the editor and then select Close Editor and then Save.

  9. Compile and run app.

    dotnet build
    dotnet run
    
  10. The app should compile, and then run, add a new message press N, and then Enter a name and message.

  11. List all the messages by pressing R then enter.

  12. Leave the app running.

Use a class to implement a retry policy

  1. In the Cloud Shell, navigate to the Java chatapp-retry folder.

    cd ~/mslearn-handle-transient-errors-in-your-app/java/chatapp-retry/
    
  2. This version includes your first draft at coding a retry policy class. It includes a config.properties configuration file to allow system administrators to set the delay and number of retries.

    number_of_retries=5
    delay=60
    
  3. Open the RetryPolicy class.

    code RetryPolicy.java
    
  4. The class keeps track of the number of retries, and handles adding a delay before code can be retried. The main logic in the canRetry method:

    public boolean canRetry() throws InterruptedException {
        // Keep track of current retries
        currentTries++;
        System.out.printf("Retrying: %s\n", currentTries);
    
        // Use a delay if this isn't the first try
        if (currentTries != 1)
        {
            TimeUnit.SECONDS.sleep(Integer.parseInt(props.getProperty("delay")));
        }
    
        if (currentTries < Integer.parseInt(props.getProperty("number_of_retries"))) {
            return true;
        } else {
            return false;
        }
    }
    

    The canRetry method checks the number of retries, and then returns true to the calling code if it's OK to keep retrying, or false if the app should now stop.

  5. Now we can use this class to add a retry policy to the chat app.

Add a retry policy

  1. Select the three ellipses (...) to the top right of the editor and then select Open in the dialog type javaChat.java, and press Enter.

  2. Locate the printAllMessages() method in javaChat.java.

  3. Replace the implementation of printAllMessages with the following code. This code adds retry logic to the MongoDB catch blocks.

        private static void printAllMessages (MongoCollection<Document> collection) throws InterruptedException {
            try {
                // Return all messages
                collection.find().forEach((Consumer<Document>) document -> {
                    System.out.printf("%s: %s\n", document.get("name"), document.get("message"));
                });
            }
            catch (com.mongodb.MongoCommandException e) {
                if (retries.canRetry())
                {
                    diagnose(e);
                    printAllMessages(collection); //retry
                } else {
                    System.out.println("Maximum retries - need to close.");
                    throw e;
                }
            }
            catch (com.mongodb.MongoSecurityException e) {
                if (retries.canRetry())
                {
                    diagnose(e);
                    printAllMessages(collection); //retry
                } else {
                    System.out.println("Maximum retries - need to close.");
                    throw e;
                }
            }
            catch (Exception e) {
                diagnose(e);
                throw e;
            }
        }
    
  4. Add a declaration for the RetryPolicy object before the Main method in javaChat.java.

    private static RetryPolicy retries;
    
  5. Instantiate the retries variable inside the Main method:

        try{
            retries = new RetryPolicy();
        } catch(FileNotFoundException e) {
            e.printStackTrace();
        }
    
  6. Add the following line of code at the top of the while loop to reset the retries.

        retries.resetRetries();
    
  7. Save the file, and close the editor. Use the commands in the ... menu in the top right corner of the editor, or use the accelerator keys Ctrl+S to save the file, and Ctrl+Q to close the editor.

  8. Build the project using the following command:

    javac -cp .:lib/* -d . javaChat.java RetryPolicy.java
    
  9. Run the app with the following command in the Cloud Shell.

    java -cp .:lib/* learn.javachatapp.javaChat
    

Use a function to implement a retry policy

  1. In the Cloud Shell, navigate to the node chatapp-retry folder.

    cd ~/mslearn-handle-transient-errors-in-your-app/node/chatapp-retry/
    
  2. This version includes your first draft at coding a retry policy class. It includes an appsettings.json configuration file to allow system administrators to set the delay and number of retries.

    {
        "number_of_retries": 5,
        "delay": 60000
    }
    
  3. Download the dependencies.

    npm install
    
  4. Open the retryPolicy.js script.

    code retryPolicy.js
    
  5. The class keeps track of the number of retries, and handles adding a delay before code can be retried. The main logic in the checkRetries function:

    
    method.checkRetries = function() {
        this._currentTries = this._currentTries + 1;
        console.log('Retrying: ' + this._currentTries);
    
        // Use a delay if this isn't the first try
        if (this._currentTries != 1)
        {
            sleep(config.delay);
        }
    
        if (this._currentTries < config.number_of_retries) {
            return true;
        } else {
            return false;
        }
    };
    

    The canRetry method checks the number of retries, and then returns true to the calling code if it's OK to keep retrying, or false if the app should now stop.

  6. Now we can use this class to add a retry policy to the chat app.

Add a retry policy

  1. Select the three ellipses (...) to the top right of the editor and then select Open in the dialog type server.js, and press Enter.

  2. Scroll down to the mongoose call to connect to the database.

    
    // Connect to MongoDB
    mongoose.connect( dbUrl, options )
      .then(() => console.log('Connection to MongoDB successful'))
      .catch(function(e) {
        console.log(e); // "error connecting to the database"
      });
    
    
  3. Add code to use the retry policy inside the catch promise.

      .catch(function(e) {
        if (retries.checkRetries()) {
          // need to retry
        } else {
          console.log(e); // "error connecting to the database"
        }
      });
    
  4. There are several ways in JavaScript to retry the code. For simplicity, this example uses recursion. Replace the connection code with the following.

    // Connect to MongoDB
    function connectWithRetry() {
      mongoose.connect( dbUrl, options )
      .then(() => console.log('Connection to MongoDB successful'))
      .catch(function(e) {
        if (retries.checkRetries()) {
          connectWithRetry();
        } else {
          console.log(e); // "error connecting to the database"
        }
      });
    }
    
    // Using the retry policy
    connectWithRetry();
    

    Tip

    There are other retry npm packages, including polly-js, that can simplify the code and offer additional features like exponential back-off.

  5. Define the retries object, and include retryPolicy.js at line 6:

    // Include packages
    var express = require('express'), http = require('http');
    var bodyParser = require('body-parser')
    var mongoose = require('mongoose');
    var app = express();
    
    // Start node app listening
    var server = http.createServer(app);
    server.listen(8000);
    server.on('error', function (err) {
      console.log(err);
    })
    
    

    By adding this code:

    // add the retry policy
    let retry = require('./retryPolicy.js');
    let retries = new retry();
    
  6. Save the file, and close the editor. Use the commands in the ... menu in the top right corner of the editor, or use the accelerator keys Ctrl+S to save the file, and Ctrl+Q to close the editor.

  7. If your browser isn't open from the previous exercise, run:

    curl -X POST http://localhost:8888/openPort/8000;
    
  8. Run the node app with:

    npm build
    npm start
    
  9. Select the returned hyperlink from the previous step.

  10. Add some messages, and refresh the page. Leave the app running.

Test the retry code

If the firewall is still switched on for the Azure Cosmos DB, the chat app can't connect to the database. Otherwise, if the chat app is still working then follow these steps to switch on the firewall.

  1. Sign into the Azure portal using the same account you used to activate the sandbox.

  2. On the Azure portal menu or from the Home page, select Azure Cosmos DB.

  3. In the list of database accounts, select the database account with a name beginning with learn-cosmos-db-.

  4. In the Azure Cosmos DB panel, select Firewall and virtual networks.

  5. In Allow access from, choose the option Selected networks.

  6. Uncheck Allow access from Azure portal.

  7. Select I understand that the current settings will block all VNets and IPs including Azure portal.

  8. Select Save to save the firewall configuration updates. These changes have enabled a firewall for the Azure Cosmos DB account, which blocks access from the Cloud Shell, simulating a connection outage.

    Note

    It can take a while for these firewall updates to complete, so wait for them to finish before proceeding to the next step.

  9. Run the app and select R to refresh all the messages. The app catches a System.TimeoutException and retries the connection to the database once again.

Based on the number-of-retries setting in the appsettings.json file of our project, the code retries connecting up to five times.

Based on the number_of_retries setting in the config.properties file of our project, the code retries connecting up to five times.

Based on the number-of-retries setting in the appsettings.json file of our project, the code retries connecting up to five times.

  1. Back on the Azure portal, select All networks, and then select Save to disable the firewall.

  2. If the app has finished, restart it.

  3. If the firewall is removed in time, the Chat App recovers, reconnects, and displays the stored messages.

Change the retry policy

  1. Quite the app, and update the retry policy configuration to only wait 5 seconds (5000 milliseconds) before each retry.
  1. Open the configuration file in the code editor.

    code appsettings.json
    
  2. Change the delay from 60000 to 5000.

  3. Save the file, and close the editor. Use the commands in the ... menu in the top right corner of the editor, or use the accelerator keys Ctrl+S to save the file, and Ctrl+Q to close the editor.

  4. Run app, and note that the retries happen much more quickly.

    dotnet run
    
  1. Open the configuration file in the code editor.

    code config.properties
    
  2. Change the delay from 60 to 5.

  3. Save the file, and close the editor. Use the commands in the ... menu in the top right corner of the editor, or use the accelerator keys Ctrl+S to save the file, and Ctrl+Q to close the editor.

  4. Run app, and note that the retries happen much more quickly.

    java -cp .:lib/* learn.javachatapp.javaChat
    
  1. Open the configuration file in the code editor.

    code appsettings.json
    
  2. Change the delay from 60000 to 5000.

  3. Save the file, and close the editor. Use the commands in the ... menu in the top right corner of the editor, or use the accelerator keys Ctrl+S to save the file, and Ctrl+Q to close the editor.

  4. Run app, and note that the retries happen much more quickly.

    npm start