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.
Release files for roundrobin 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| roundrobin-0.1.0.tar.gz | 10.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| roundrobin-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.9 kB
Release files / roundrobin-0.1.0.tar.gz
| Download URL | roundrobin-0.1.0.tar.gz |
|---|---|
| Size | 10.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
14126ed1706a9c6638c81c3f3d915357f7ab0c0245c464a611eb5903811ee45b
|
|
BLAKE2b-256 checksum How to use checksums |
30bbf9d47ebed19fcdb55d05816b9b548631d91ae18aaa51a39e31dee8eb884f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jan 25, 2026.
Transparency logRelease files / roundrobin-0.1.0-py3-none-any.whl
| Download URL | roundrobin-0.1.0-py3-none-any.whl |
|---|---|
| Size | 10.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8a1809c874fe63aee401bb40eeacc60d6bc5640fbef96af680d0e81661bdc1ef
|
|
BLAKE2b-256 checksum How to use checksums |
3d4624d99ea14bf5dd925415173812e76efd52c748014ec95b1b5be546d6d0b6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jan 25, 2026.
Transparency log