Collection of roundrobin utilities
Project description
roundrobin
A small collection of round-robin selectors:
basic()- plain round-robin (A, B, C, A, B, C, ...)weighted()- weighted round-robin (classic LVS-style)smooth()- smooth weighted round-robin (Nginx-style; avoids long clumps)smooth_stateful()- smooth weighted RR with runtime controls (health checks / slow-start)
Install
pip install roundrobin
Quickstart
>>> 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'
Stateful smooth weighted round-robin
Use smooth_stateful() when you want smooth weighted balancing but also need to change weights at runtime
(health checks, slow-start, draining, etc.). It returns a selector object that you call repeatedly to
pick the next key.
The key idea is that you can temporarily reduce a key's influence without changing its long-term configured capacity, and the selector will smoothly converge to the new distribution over subsequent calls.
What you do:
- call
rr()to get the next key - use
rr.set(key, weight=..., effective=...)to adjust weights - use
rr.disable(key)to take items out of rotation - use
rr.enable(key, effective_weight=0)to re-enable (0 for slow-start; omit for full weight) - use
rr.reset(key)to restoreeffective_weightto full (the current configuredweight)
Terminology:
- weight - the configured (base) weight representing target capacity
- effective_weight - the weight used for selection right now (clamped to
[0, weight])
Behavior:
- Lowering
effective_weight(explicitly viaset(..., effective=...)or implicitly by loweringweight) takes effect immediately; the selector also adjusts its accumulated state so a previously heavy key doesn't keep dominating after being down-weighted. - If
effective_weight < weight, it auto-ramps by +1 after everyrr()call until it reachesweightagain (unless the item is disabled).
Statefulness note: the selector keeps per-item state, so sequences after changes depend on prior calls. These methods affect future picks; they do not "restart" the schedule. If you want a clean slate after changes, create a new selector instance. The examples below create a new selector for each scenario so outputs are easy to reason about.
>>> take = lambda source, n: ''.join([source() for _ in range(n)])
>>> rr = roundrobin.smooth_stateful([("A", 5), ("B", 1), ("C", 1)])
>>> take(rr, 14)
'AABACAAAABACAA'
>>> # set() changes weights for future picks
>>> rr = roundrobin.smooth_stateful([("A", 5), ("B", 1), ("C", 1)])
>>> rr.set("A", weight=2)
>>> take(rr, 14)
'ABCAABCAABCAAB'
>>> # disable() removes an item from rotation
>>> rr = roundrobin.smooth_stateful([("A", 5), ("B", 1), ("C", 1)])
>>> rr.disable("A")
>>> take(rr, 14)
'BCBCBCBCBCBCBC'
>>> # enable() can bring an item back with slow-start (start at 0 and ramp up)
>>> rr = roundrobin.smooth_stateful([("A", 5), ("B", 1), ("C", 1)])
>>> rr.disable("A")
>>> rr.enable("A", effective_weight=0)
>>> take(rr, 14)
'BCAABAACAAABAA'
>>> # initial_effective supports None | int | dict | callable
>>> rr = roundrobin.smooth_stateful([("A", 3), ("B", 1)], initial_effective=0)
>>> take(rr, 14)
'AABAAABAAABAAA'
>>> rr = roundrobin.smooth_stateful([("A", 3), ("B", 1)], initial_effective={"B": 0})
>>> take(rr, 14)
'AAABAAABAAABAA'
>>> rr = roundrobin.smooth_stateful(
... [("A", 5), ("B", 1), ("C", 1)],
... initial_effective=lambda key, weight: 0 if key == "A" else weight,
... )
>>> take(rr, 14)
'BCAABAACAAABAA'
>>> # reset() restores effective_weight back to the current configured weight
>>> rr = roundrobin.smooth_stateful([("A", 5), ("B", 1)])
>>> item_a = rr.items[0] # "A"
>>> rr.set("A", effective=0)
>>> item_a.effective_weight
0
>>> rr.reset("A")
>>> item_a.effective_weight
5
>>> # effective_weight is clamped to [0, weight] and auto-ramps by +1 per call
>>> rr = roundrobin.smooth_stateful([("A", 3), ("B", 1)])
>>> rr.set("A", effective=0)
>>> take(rr, 14)
'BABAAABAAABAAA'
>>> [item.effective_weight for item in rr.items]
[3, 1]
>>> # methods raise KeyError when no items match key
>>> rr = roundrobin.smooth_stateful([("A", 1), ("B", 1)])
>>> rr.set("missing", weight=1)
Traceback (most recent call last):
...
KeyError: 'missing'
>>> # keys are matched by equality, so unhashable keys work
>>> a = {"name": "A"}
>>> b = {"name": "B"}
>>> rr = roundrobin.smooth_stateful([(a, 1), (b, 1)])
>>> rr.disable({"name": "A"})
>>> rr() is b
True
>>> # dict-based initial_effective requires hashable keys
>>> roundrobin.smooth_stateful([([1, 2], 1)], initial_effective={[1, 2]: 0})
Traceback (most recent call last):
...
TypeError: ...unhashable type: 'list'...
Gotchas
>>> # empty datasets are rejected
>>> roundrobin.basic([])
Traceback (most recent call last):
...
ValueError: dataset must be non-empty
>>> # weights must be non-negative integers
>>> roundrobin.smooth([("A", -1)])
Traceback (most recent call last):
...
ValueError: weights must be non-negative
>>> # weighted() falls back to basic RR when all weights are equal (including 0)
>>> rr = roundrobin.weighted([("A", 0), ("B", 0)])
>>> take(rr, 4)
'ABAB'
>>> # if total effective weight is 0 (e.g. initial_effective=0),
>>> # smooth() / smooth_stateful() fall back to the first item to avoid returning None
>>> rr = roundrobin.smooth_stateful([("A", 1), ("B", 1)], initial_effective=0)
>>> take(rr, 6)
'AABABA'
>>> # smooth_stateful() returns None only when there are no candidates (all disabled)
>>> rr = roundrobin.smooth_stateful([("A", 1), ("B", 1)])
>>> rr.disable("A")
>>> rr.disable("B")
>>> print(rr())
None
>>> # effective=0 is temporary unless the item is disabled (it auto-ramps by +1 per call)
>>> rr = roundrobin.smooth_stateful([("A", 3), ("B", 1)])
>>> rr.set("A", effective=0)
>>> for _ in range(3):
... _ = rr()
>>> [item.effective_weight for item in rr.items]
[3, 1]
>>> rr.disable("A")
>>> for _ in range(3):
... _ = rr()
>>> [item.effective_weight for item in rr.items]
[0, 1]
>>> # setting weight=0 removes an item from selection (without using disable())
>>> rr = roundrobin.smooth_stateful([("A", 1), ("B", 1)])
>>> rr.set("A", weight=0)
>>> take(rr, 4)
'BBBB'
Thread Safety
>>> # selectors are not thread-safe; wrap calls with a lock when shared
>>> import threading
>>> rr = roundrobin.smooth_stateful([("A", 2), ("B", 1)])
>>> lock = threading.Lock()
>>> def safe_next():
... with lock:
... return rr()
>>> safe_next()
'A'
>>> # use per-thread instances
>>> import threading
>>> def worker():
... rr = roundrobin.basic(["A", "B"])
... return rr()
>>> t = threading.Thread(target=worker)
>>> t.start()
>>> t.join()
License
MIT. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file roundrobin-0.1.0.tar.gz.
File metadata
- Download URL: roundrobin-0.1.0.tar.gz
- Upload date:
- Size: 10.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14126ed1706a9c6638c81c3f3d915357f7ab0c0245c464a611eb5903811ee45b
|
|
| MD5 |
320a624812f99d8e4f13154501fdb6c1
|
|
| BLAKE2b-256 |
30bbf9d47ebed19fcdb55d05816b9b548631d91ae18aaa51a39e31dee8eb884f
|
Provenance
The following attestation bundles were made for roundrobin-0.1.0.tar.gz:
Publisher:
pypi.yml on linnik/roundrobin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
roundrobin-0.1.0.tar.gz -
Subject digest:
14126ed1706a9c6638c81c3f3d915357f7ab0c0245c464a611eb5903811ee45b - Sigstore transparency entry: 852869807
- Sigstore integration time:
-
Permalink:
linnik/roundrobin@351ac17bf99faf8409744a8fbdc0d41046a05f00 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/linnik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@351ac17bf99faf8409744a8fbdc0d41046a05f00 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file roundrobin-0.1.0-py3-none-any.whl.
File metadata
- Download URL: roundrobin-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a1809c874fe63aee401bb40eeacc60d6bc5640fbef96af680d0e81661bdc1ef
|
|
| MD5 |
2aa21e2fa1c12ce63ac223bfc188f2c0
|
|
| BLAKE2b-256 |
3d4624d99ea14bf5dd925415173812e76efd52c748014ec95b1b5be546d6d0b6
|
Provenance
The following attestation bundles were made for roundrobin-0.1.0-py3-none-any.whl:
Publisher:
pypi.yml on linnik/roundrobin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
roundrobin-0.1.0-py3-none-any.whl -
Subject digest:
8a1809c874fe63aee401bb40eeacc60d6bc5640fbef96af680d0e81661bdc1ef - Sigstore transparency entry: 852869811
- Sigstore integration time:
-
Permalink:
linnik/roundrobin@351ac17bf99faf8409744a8fbdc0d41046a05f00 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/linnik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@351ac17bf99faf8409744a8fbdc0d41046a05f00 -
Trigger Event:
workflow_dispatch
-
Statement type: