How to mitigate risk with progressive feature rollouts in Python using LaunchDarkly. featured image

Overview

“Works on my machine” can be a standard cop-out for remedying problems within your code base. While yes, things can work great in a small silo errr — your machine, rolling out features at scale can increase the potential for risk as more users access a new feature or application.  Roll out new features confidently by leveraging progressive rollouts to identify any issues that may occur as you scale. Let’s work through how to implement progressive rollouts within your Python application using LaunchDarkly. 

Implementing Progressive Rollout to your Python Application.

Let’s walk through how to implement a progressive rollout feature that slowly migrates DJ Toggle’s Playlist from a static JSON file to a more comprehensive database using Python and LaunchDarkly. When complete, our application will roll out a percentage of users at a time, mitigating risk by slowly increasing the number of individuals with access to the new feature.

Sample Project Repository

The assets needed for this project and a completed sample are in the LaunchDarkly Labs Risk Management Python Repository on GitHub. 

Additional assets for this tutorial are in the folder `0-Tutorial-Assets` within this repository, and the project folder is under `Progressive-Rollout > Python.`

Getting Started with LaunchDarkly

To prevent context switching, we’ll start by setting up our LaunchDarkly account, getting our SDK key, and setting up our first flag. We’ll then build out our basic application and walk through how to add a feature flag.

Start by heading to LaunchDarkly and creating your account (or log in if you already have one). If you're new, click ‘Skip setup’ after verifying your email.

You will then be added to a default project. 

To get the relevant SDK keys for your project:

  1. Click on the cog icon on the lower left side of the screen.
  2. Navigate to "Projects" and click on your project's name.

Once you click on the project name, you’ll be brought to the project settings page. 

For this tutorial, we'll be working within the production environment.
Click on the three dots next to your environment and select ‘SDK key.’

Setting up your environment

First things first, let's set up our project environment:

Navigate to the directory and create a virtual environment:

# Create a new directory for your project
mkdir risk-management-python

# Navigate to the new directory
cd risk-management-python

# Create a virtual environment
python -m venv venv

Activate the virtual environment:

On Mac/Linux:

source venv/bin/activate

On Windows:

venv\Scripts\activate

Install the required packages:

pip install launchdarkly-server-sdk python-dotenv

Finally, freeze these requirements to the requirements folder

pip freeze > requirements.txt

In this project folder, create a .env file by writing the following within your terminal.

echo "LD_SDK_KEY=your_actual_sdk_key_here" > .env

Hint — the LD_SDK_KEY begins with the characters “sdk-” if you’re stuck. 

Create a Feature Flag for your Progressive Rollout

Creating a feature flag

To create a feature flag that will enable you to progressively rollout your database

  1. Click on "Create flag."
  2. Name your flag progressive-rollout.
  3. Select the following when it comes to flag setup.
  4. Flag Configuration: Release
  5. Is this flag temporary?: Yes
  6. Flag Type: Boolean
  7. Variations:
  8. New Database: true
  9. Legacy JSON file: false
  10. Default Variations:
  11. When flag is off: Legacy JSON file
  12. When flag is on: New Database

Setting up the progressive rollout

After you’ve created your flag, navigate to the right-hand panel of the flag details screen and click on `Manage workflows.’ This will open a new page where you can create your progressive rollout workflow.

Once you’ve selected ‘Manage workflows’ you’ll be brought to the workflow screen. From here, select ‘Progressive rollout’. A modal will appear and you will be able to configure the full timeline and percentages of each roll out at once.  

Creating the application

Within the main project folder,  create our main.py file and start building our application. 

Want to see the finished application code all at once? Scroll down or navigate to the main.py file within the `Progressive-Rollout > Python` filepath

Import the necessary modules:

from dotenv import load_dotenv
import json
import sqlite3
import os
import ldclient
from ldclient.config import Config
from ldclient import Context

Initialize the LaunchDarkly Client:

load_dotenv()
ld_sdk_key = os.environ.get('LD_SDK_KEY')
if not ld_sdk_key:
    raise ValueError("LD_SDK_KEY not found in environment variables")

ldclient.set_config(Config(ld_sdk_key))
ld_client = ldclient.get()

if ld_client.is_initialized():
    print("LaunchDarkly client initialized successfully")
else:
    print("LaunchDarkly client failed to initialize")
    exit(1)

Copy the `dj-toggles-top-30.json` file into your working directory from the provided tutorial asset folder.

Then, load it into your working main.py file by adding the following:

with open('dj_toggles_top_30.json', 'r') as file:
    tracklist = json.load(file)

Set up a SQLite Database with this information.

While setting up the database, be sure to set it up so that if it runs multiple times, it does not add duplicate information to the database.

def create_database():
    conn = sqlite3.connect('tracklist.db')
    c = conn.cursor()
    
    c.execute('''CREATE TABLE IF NOT EXISTS tracks
                 (track_name TEXT, track_length TEXT, artist TEXT, album_name TEXT, track_number INTEGER, release_date TEXT)''')
    
    c.execute('''DELETE FROM tracks WHERE rowid NOT IN 
                 (SELECT MIN(rowid) FROM tracks GROUP BY track_name, artist)''')
    
    c.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_track_artist 
                 ON tracks(track_name, artist)''')
    
    for track in tracklist:
        c.execute('''INSERT OR IGNORE INTO tracks 
                     (track_name, track_length, artist, album_name, track_number, release_date)
                     VALUES (?, ?, ?, ?, ?, ?)''', 
                  (track['track_name'], track['track_length'], track['artist'], 
                   track['album_name'], track['track_number'], track['release_date']))
    
    conn.commit()
    conn.close()

create_database()

Create functions to pull the information from the JSON file and the SQLite database.

def get_tracks_from_json():
    return [track['track_name'] for track in tracklist[:10]]

def get_tracks_from_db():
    conn = sqlite3.connect('tracklist.db')
    c = conn.cursor()
    c.execute("SELECT track_name FROM tracks")
    tracks = [row[0] for row in c.fetchall()]
    conn.close()
    return tracks

Now, we’ll create the main application logic in the function `run_app`.

def run_app():
    user = Context.builder('context-key-123').set('groups', ['beta_testers']).build()
    use_database = ld_client.variation("use-database", user, False)

    if use_database:
        print("The whole playlist")
        tracks = get_tracks_from_db()
    else:
        print("The top 10")
        tracks = get_tracks_from_json()

    print("DJ Toggle's Top Tracks")
    for i, track in enumerate(tracks, 1):
        print(f"{i}. {track}")

if __name__ == "__main__":
    run_app()
    ld_client.close()

This function leverages LaunchDarkly to switch between different data sources for the playlist based on the value of a feature flag.

Let’s walk through this function step by step:

  1. First, create a Context object. This represents a user or context for evaluating feature flags. In this case, the user has an identifier (`context-key-123`) and is part of the 'beta_testers' group. LaunchDarkly uses this context to determine which feature flags should be active for this user.
user = Context.builder('context-key-123').set('groups', ['beta_testers']).build()
  1. Next, evaluate the feature flag by checking the value of the “progressive-rollout” feature flag for the given user context. If the flag is enabled for this user, `progressive-rollout` will be `True`; otherwise, it will be `False`. The third argument, False, is the default value used if the flag is not found or there's an error reading the argument.
use_database = ld_client.variation("progressive-rollout", user, False)
  1. Depending on the value of `progressive-rollout`, the application decides whether to fetch the playlist from the database or a JSON file. If `progressive-rollout` is `True`, it fetches the complete playlist from the SQLite database `(get_tracks_from_db())`. If `use_database` is `False`, it fetches only the top 10 tracks from the JSON file `(get_tracks_from_json())`.
print("DJ Toggle's Top Tracks")
for i, track in enumerate(tracks, 1):
print(f"{i}. {track}")

if __name__ == "__main__:
run_app()
ld_client.close()

Running the application. 

To run the application, run `python main.py` within your terminal and watch the output. 

The output will be pulled from the JSON file if the feature flag is turned off within the LaunchDarkly application.

If the new feature — pulling from the SQLite database — is turned on, you will see the full tracklist as an output. 

Check out the complete Python application below:

import json
import sqlite3
import os
import ldclient
from ldclient.config import Config
from ldclient import Context




## Initialize LaunchDarkly client using the SDK key from your .env file


ld_sdk_key = os.environ.get('LD_SDK_KEY')
if not ld_sdk_key:
   raise ValueError("LD_SDK_KEY not found in environment variables")


ldclient.set_config(Config(ld_sdk_key))
ld_client = ldclient.get()


if ld_client.is_initialized():
   print("LaunchDarkly client initialized successfully")
else:
   print("LaunchDarkly client failed to initialize")
   exit(1)


## Load tracklist from your JSON file
with open('dj_toggles_top_30.json', 'r') as file:
   tracklist = json.load(file)


## Set up SQLite database
def create_database():
   conn = sqlite3.connect('tracklist.db')
   c = conn.cursor()
  
   # Create the table if it doesn't exist
   c.execute('''CREATE TABLE IF NOT EXISTS tracks
                (track_name TEXT, track_length TEXT, artist TEXT, album_name TEXT, track_number INTEGER, release_date TEXT)''')
  
   # Remove existing duplicates
   c.execute('''DELETE FROM tracks WHERE rowid NOT IN
                (SELECT MIN(rowid) FROM tracks GROUP BY track_name, artist)''')
  
   # Create the unique index
   c.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_track_artist
                ON tracks(track_name, artist)''')
  
   # Insert new tracks, ignoring duplicates
   for track in tracklist:
       c.execute('''INSERT OR IGNORE INTO tracks
                    (track_name, track_length, artist, album_name, track_number, release_date)
                    VALUES (?, ?, ?, ?, ?, ?)''',
                 (track['track_name'], track['track_length'], track['artist'],
                  track['album_name'], track['track_number'], track['release_date']))
  
   conn.commit()
   conn.close()


create_database()


## Get tracks from the JSON and database


def get_tracks_from_json():
   return [track['track_name'] for track in tracklist[:10]]


def get_tracks_from_db():
   conn = sqlite3.connect('tracklist.db')
   c = conn.cursor()
   c.execute("SELECT track_name FROM tracks")
   tracks = [row[0] for row in c.fetchall()]
   conn.close()
   return tracks


## Main app logic, note how there is two different versions, a use database version and use JSON version. 


from ldclient.context import Context


def run_app():
   user = Context.builder('context-key-123').set('groups', ['beta_testers']).build()
   use_database = ld_client.variation("progressive-rollout", user, False)


   if use_database:
       print("The whole playlist")
       tracks = get_tracks_from_db()
   else:
       print("The top 10")
       tracks = get_tracks_from_json()


   print("DJ Toggle's Top Tracks")
   for i, track in enumerate(tracks, 1):
       print(f"{i}. {track}")


## run the application and close the client
if __name__ == "__main__":
   run_app()
   ld_client.close()

Monitor and adjust

As the rollout progresses, monitor your application's performance and user feedback.

Use LaunchDarkly's dashboard to view the current status of the rollout.

If issues arise, you can pause or roll back the changes from the LaunchDarkly dashboard without changing your code.

What’s happening under the hood?

When implementing a progressive rollout using LaunchDarkly, you slowly drip out the new feature to a percentage of users at a time, allowing you to monitor for any issues or performance degradation gradually.

In the example we created together, DJ Toggle's playlist is served from either a JSON file or a SQLite database, depending on the state of the feature flag. As the rollout progresses (10% of our audience at a time), more users will see the full playlist from the database. If anything goes wrong, you can quickly switch users back to the JSON version without breaking a sweat (or a beat).

To sum things up

When rolling out new features, it’s natural to be anxious about unleashing your latest masterpiece on the masses. By leveraging progressive rollouts, you don’t have to worry about a sudden increase in errors or bugs (or, in the case of DJ Toggle’s playlists — a sudden onslaught of obscure B-sides from the 80s.) Instead, you can introduce your new feature gradually, ensuring everything works perfectly before reaching the entire audience.
Want to geek out on progressive rollouts, risk management, or what obscure playlist you’re jamming to right now? I’m always here to help. Email me at emikail@launchdarkly.com, @ me on Twitter or join the LaunchDarkly Discord.

Like what you read?
Get a demo
Related Content

More about De-risked releases

August 15, 2024