Skip to content

Commit

Permalink
add round-robin utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
linnik committed Feb 25, 2019
0 parents commit 5163b1b
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.pyc
.DS_Store
.pyre
.pyre_configuration
.watchmanconfig
__pycache__
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
This is rather small collection of round robin utilites

```python
>>> import roundrobin
>>> get_roundrobin = roundrobin.basic(["A", "B", "C"])
>>> ''.join([get_roundrobin() for _ in range(7)])
'ABCABCA'
>>> # weighted round-robin balancing algorithm as seen in LVS
>>> get_weighted = roundrobin.weighted([("A", 5), ("B", 1), ("C", 1)])
>>> ''.join([get_weighted() for _ in range(7)])
'AAAAABC'
>>> # smooth weighted round-robin balancing algorithm as seen in Nginx
>>> get_weighted_smooth = roundrobin.smooth([("A", 5), ("B", 1), ("C", 1)])
>>> ''.join([get_weighted_smooth() for _ in range(7)])
'AABACAA'
```

5 changes: 5 additions & 0 deletions roundrobin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from roundrobin.basic_rr import basic
from roundrobin.smooth_rr import smooth
from roundrobin.weighted_rr import weighted

__ALL__ = [basic, weighted, smooth]
11 changes: 11 additions & 0 deletions roundrobin/basic_rr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from itertools import cycle, islice
from typing import Any, Callable


def basic(dataset: [Any]) -> Callable[[], Any]:
iterator = cycle(dataset)

def get_next():
return next(iterator)

return get_next
41 changes: 41 additions & 0 deletions roundrobin/smooth_rr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Callable, Optional, Tuple


def smooth(dataset: [Tuple[str, int]]) -> Callable[[], Optional[str]]:
dataset_length = len(dataset)
dataset_extra_weights = [ItemWeight(*x) for x in dataset]

def get_next() -> Optional[str]:
if dataset_length == 0:
return None
if dataset_length == 1:
return dataset[0][0]

total_weight = 0
result = None
for extra in dataset_extra_weights:
extra.current_weight += extra.effective_weight
total_weight += extra.effective_weight
if extra.effective_weight < extra.weight:
extra.effective_weight += 1
if not result or result.current_weight < extra.current_weight:
result = extra
if result:
result.current_weight -= total_weight
return result.key
return None

return get_next


class ItemWeight:
key: str
weight: int
current_weight: int
effective_weight: int

def __init__(self, key, weight):
self.key = key
self.weight = weight
self.current_weight = 0
self.effective_weight = weight
31 changes: 31 additions & 0 deletions roundrobin/weighted_rr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import math
from typing import Callable, Optional, Tuple


def weighted(dataset: [Tuple[str, int]]) -> Callable[[], Optional[str]]:
current_index = -1
current_weight = 0
dataset_length = len(dataset)
dataset_max_weight = 0
dataset_gcd_weight = 0

for _, weight in dataset:
if dataset_max_weight < weight:
dataset_max_weight = weight
dataset_gcd_weight = math.gcd(dataset_gcd_weight, weight)

def get_next() -> Optional[str]:
nonlocal current_index
nonlocal current_weight
while True:
current_index = (current_index + 1) % dataset_length
if current_index == 0:
current_weight = current_weight - dataset_gcd_weight
if current_weight <= 0:
current_weight = dataset_max_weight
if current_weight == 0:
return None
if dataset[current_index][1] >= current_weight:
return dataset[current_index][0]

return get_next
24 changes: 24 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pathlib

import setuptools

HERE = pathlib.Path(__file__).parent
README = (HERE / "README.md").read_text()

setuptools.setup(
name="roundrobin",
version="0.0.1",
description="Collection of roundrobin utilities",
long_description=README,
long_description_content_type="text/markdown",
url="https://github.com/linnik/roundrobin",
author="Vyacheslav Linnik",
author_email="[email protected]",
license="MIT",
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
],
packages=setuptools.find_packages(),
)
48 changes: 48 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import unittest

import roundrobin


class WeightCase(unittest.TestCase):

def test_basic(self):
data = ["A", "B", "C"]
get_next = roundrobin.basic(data)
result = ''.join([get_next() for _ in range(7)])
self.assertEqual(result, 'ABCABCA')

def test_smooth(self):
data = [("A", 5), ("B", 1), ("C", 1)]
get_next = roundrobin.smooth(data)
result = ''.join([get_next() for _ in range(7)])
self.assertEqual(result, 'AABACAA')

def test_weighted_not_smooth(self):
data = [("A", 5), ("B", 1), ("C", 1)]
get_next = roundrobin.weighted(data)
result = ''.join([get_next() for _ in range(7)])
self.assertEqual(result, 'AAAAABC')

def test_weighted(self):
data = [("A", 50), ("B", 35), ("C", 15)]
get_next = roundrobin.weighted(data)

cnt = {}
for _ in range(100):
key = get_next()
cnt[key] = cnt.get(key, 0) + 1
self.assertTrue(cnt['A'] == 50)
self.assertTrue(cnt['B'] == 35)
self.assertTrue(cnt['C'] == 15)

cnt = {}
for _ in range(200):
key = get_next()
cnt[key] = cnt.get(key, 0) + 1
self.assertTrue(cnt['A'] == 100)
self.assertTrue(cnt['B'] == 70)
self.assertTrue(cnt['C'] == 30)


if __name__ == "__main__":
unittest.main()

0 comments on commit 5163b1b

Please sign in to comment.