Faster python app

Python language logo

Most of the developers do not use cached_property and lru_cache from functools standard library but also does not cache HTTP request/response into outside file/database. Example in this article are tested under Python 3.8

Usage functools.cached_property

Let say you have an intensive calculation. It takes time and CPU usage. It happens all the time. There is a need to calculate some values for webshop each time the client access site. Example usage of cached_property:

from functools import cached_property
import statistics
from time import time

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)


numbers = range(1,10000)
testDataSet = DataSet(numbers)

start = time()
result = testDataSet.stdev
result = testDataSet.variance
end = time()
print(f"First run: {(end - start):.6f} second")

start = time()
result = testDataSet.stdev
result = testDataSet.variance
end = time()
print(f"Second run: {(end - start):.6f} second")

start = time()
result = statistics.stdev(numbers)
result = statistics.variance(numbers)
end = time()
print(f"RAW run: {(end - start):.6f} second")

Output would look similar to this:

First run: 0.247226 second
Second run: 0.000002 second
RAW run: 0.242232 second

You can run code online: Python code example IDE Online

Usage functools.lru_cache

lru_cache is a decorator that is used for function using memoizing callable that saves up to the maxsize most recent calls. Again you have a lot of calculation and you want to save some results (the example we calculate N and N+1 we need just one step instead of re-calculating complete N+1) of early calculation that helps us to build next result with cached ones.

from functools import lru_cache
from time import time

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)


start = time()
result = [fib(n) for n in range(40000)]
end = time()
print(f"First run: {(end - start):.6f} second")

start = time()
result = [fib(n) for n in range(40000)]
end = time()
print(f"Second run: {(end - start):.6f} second")

start = time()
result = [fib(n) for n in range(39999)]
end = time()
print(f"Third run: {(end - start):.6f} second")


start = time()
result = [fib(n) for n in range(40001)]
end = time()
print(f"Fourth run: {(end - start):.6f} second")
print(fib.cache_info())

Output would be:

First run: 0.278697 second
Second run: 0.017155 second
Third run: 0.017530 second
Fourth run: 0.065415 second
CacheInfo(hits=199997, misses=40001, maxsize=None, currsize=40001)

The first call is cached. The second one is re-using cache, the third one is N-1 and the fourth is N+1.

As we can see in the last 3 cases - we re-use cache. This could be used for database, calculation, any CPU usage that we want to repeat or operation we want to keep in cache.

Here is an online IDE you can run and view: lru_cache example

HTTP request caching

With lru_cache we could also cache web requests for static pages. Other options are to keep the result in the file based on our input data.

Let us see first options:

from functools import lru_cache
import urllib.request
from time import time

@lru_cache(maxsize=32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'http://www.python.org/dev/peps/pep-%04d/' % num
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'
start = time()
for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
     pep = get_pep(n)
     #print(n, len(pep))
end = time()
print(f"First run: {(end - start):.6f} second")

print(get_pep.cache_info())     

print("\n")

start = time()
for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
     pep = get_pep(n)
     #print(n, len(pep))     
end = time()
print(f"Second run: {(end - start):.6f} second")

print(get_pep.cache_info())

If we run this code, we get:

First run: 0.897728 second
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)

Second run: 0.000026 second
CacheInfo(hits=14, misses=8, maxsize=32, currsize=8)

You can run this code: HTTP Caching

Now let us talk about real projects in real life. You have IP or word and you need to check or to get a replacement. But you have 2^32-1 IP or 50 million words. And you don't want to lose all information you got from these services. But caching inside of python is not enough for this. So what are we going to do? We put the result in a file or database.

Example code:

import urllib.request
from time import time

def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'http://www.python.org/dev/peps/pep-%04d/' % num
    f = ""
    ff = ""
    try:
       f = open(str(num),"r")
       txt_file = f.read()       
       return txt_file
       # Do something with the file
    except IOError:
       nothing = "a"

    try:
        with urllib.request.urlopen(resource) as s:
            ff = open(str(num),"w+")
            txt = s.read()
            ff.write(str(txt))
            return txt
    except urllib.error.HTTPError:
        return 'Not Found'


start = time()
for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
     pep = get_pep(n)
end = time()
print(f"First run: {(end - start):.6f} second")

print("\n")

start = time()
for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
     pep = get_pep(n)
end = time()
print(f"Second run: {(end - start):.6f} second")

You can run code caching results from http This code produce something similar to:

First run: 4.196623 second
Second run: 0.358382 second

Why is this better ? in short: if you have 20 million keys, words, something and you run day by day - then it is better to keep in database or files. This example (file, writing to file) is the simplest proof of concept. I am lazy to implement MySQL, PostgreSQL, or SQLite records to keep.

Python dictionary algorithm and hash table vulnerability

Hash function is taken from www.tutorialspoint.com Picture of a hash function is taken from www.tutorialspoint.com

Structure commonly used inside of most languages is a hash table or in the python dictionary (dict). It exists in Java, PHP, Ruby, and so on. We going to review dict structure under python and then to explain how this attack is used to DDOS.

Python dict

Dict or hash table (under different programming language) is an algorithm that goes in best case O(1) and worst-case O(N). N is several key-element inside of dict. This is example how dict under python works.

our_dict = {} 

So we create dict object that have 2 parts: value and key (this is just to show structue as example)

our_dict = { None: None,
             None: None,
             None: None,
             None: None
            } 

With the proper key, we can get value connected to that key. Example to add "apple" as key and value 3. Dict has 4 places.

>>> our_dict = {}
>>> our_dict["apple"] = 3
>>> hash("apple") % 4
2
>>> 

In our case place 2 (it start from 0 and it goes to 2, so third place)

our_dict = { None: None,
             None: None,
             "apple" : 3,
             None: None
            } 

So we do up to 50% places.

our_dict = { None: None,
             "kiwi": 4,
             "apple" : 3,
             None: None
            } 

And next one we add:

>>> hash("pear") % 4
1

but we have on place 1. Now we extend place 1 with list-like object.

our_dict = { None: None,
             [["kiwi": 4], ["pear": 7]],
             "apple" : 3,
             None: None
            } 

In short, each time we add value and it makes collision place is extend to list. When it goes full it makes double the current size and re-hashed key/values. This happens automatically and in that case all keys are re-hashed.

This is the shortest description of this kind of structure.

How dict or hash table are used for DDOS

This is used for every web server and script language. So you send some request, web server it cached with the hash table (because of each request is cached if does not exist - if request exist, it just pull from memory and serve browser with no PHP /Go/Python/Java execution)

But what if the attacker knows how to make collision inside of web server and full 1 million or maximum possible queue on that web server?

So he sends a request, if not cached web server cached. Each time when it checks it grows N+1. So it needs to make N checks. But if a collision happens it goes to check trough list. Each time key/value is added to place into the list. So this makes O(N^2) to insert key/value in the hash table. This starts slowly to slow down fast O(1) and use CPU more and more.

The first time is discovered in a 2003 paper Denial of Service via Algorithmic Complexity Attacks

After adding some random bytes to keep an attacker away from this (each time hash table has unique hashing value on each server) in 2011 they find a way to bypass. Youtube link from 28th Chaos Communication Congress 28c3: Effective Denial of Service attacks against web application platforms

In response to this - they add "secret randomized seed value" to keep attack away.

But in 2012 ( congress 29c3) on they use differential cryptanalysis to crack this random seed and to use for the attack.

Later they suggest using SipHash as a good combination of speed and security. So it resists to a DDOS attack on hash table / dict. All programming languages and web services using SipHash to prevent the attack. Url info on the attack: Hash table attack

Python: new and old things

Python logo

There are several things that need to be bolded for every py programmer to be used or to have information on how and why is used.

Type hints

Python 3.5 brings type hints for clear function/interface usage. Idea is to set proper input instead of sending different type variables. Example of bad code:

def greeting(name):
    return 'Hello ' + name

Here we can pass numbers, boolean, or another complex object (class, function) and produce issue.

The clean version goes:

def greeting(name: str) -> str:
    return 'Hello ' + name

If you have 1000 lines inside of your codebase, for sure you don't understand this approach. But if your code goes up to 10 million lines of code - this seems really good practice.

Assignment expressions

In version Python 3.8 you have assignment expressions where is possible to use ":=" as part of a larger expression.

if (n := len(a)) > 10:
    print(f"List is too long ({n} elements, expected <= 10)")

This is good and not good. Let me explain why. A a person who lives in Py world, this lead me to think what would do programmer that don't take time to make clean code. Example of growing expression:

if (n := len(c := (d := a.replace("a","g"))[:len(a)])) > 10:
    print(f"List is too long ({n} elements, expected <= 10)")

Clean code could be:

a = "asdasdasdasdasdasd"
d = a.replace("a","g") 
c = d[:len(d)]

if (n := len(c)) > 10:
    print(f"List is too long ({n} elements, expected <= 10)")

This is why a new approach could lead to messy code.

range() and xrange() in Py2.x

People still using Python 2.x. Some of the usages are in web services. Sometimes they need to produce a bunch of IDs from 1 to 100 million. One approach is to use range() which return list. xrange() return generator object. What is the difference between them? In the first case, range, it uses large amounts of memory to set all numbers (integer, 4 bytes) into memory. So 100 000 000 x 4 is a lot of memory. Generator object objects that produce next() value and using just one size of integer for memory. This is used for example subnet generator, and picking up the second IP from the list. So imagine if you have 1000 subnets to generate each time with range() and xrange(). This happened before and I saw the issue. Change from range() to xrange() brings better work with memory and stopped to crashing web services.

New Dictionary algorithm

In Python 3.6 is introduced a new algorithm for dict objects. Proposed by Raymond Hettinger on Py Dev list New Dict proposal it brings between 20% and 25% smaller compared to Python 3.5. Good video on this topic is Raymond Hettinger Modern Python Dictionaries

asyncio — Asynchronous I/O, event loop, coroutines and tasks

It was introduced in Python 3.4. From the official documentation: "This module provides the infrastructure for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources, running network clients and servers, and other related primitives" Asyncio usage