Skip to main content

Tests Coverage PyPI install versions

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 restore effective_weight to full (the current configured weight)

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 via set(..., effective=...) or implicitly by lowering weight) 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 every rr() call until it reaches weight again (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)

Source distribution for roundrobin 0.1.0
File Size Uploaded
roundrobin-0.1.0.tar.gz 10.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for roundrobin 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.4

1 release file

0.0.3

1 release file

0.0.2

1 release file

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page