Repository navigation
feat(gateway): computers follow the clock through a watch, and clock writes wait for them (FD-5471) #614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
earakely-scale
merged 11 commits into
main
from
edgararakelyan/fd-5471-hb11-gateway-clock-one-version-a
Nov 11, 2026
Merged
feat(gateway): computers follow the clock through a watch, and clock writes wait for them (FD-5471) #614
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
ae243d2
feat(gateway): computers follow the clock through a watch, and clock …
earakely-scale 91428dc
fix(gateway): read CLOCK_FOLLOWER_TOKENS with a parser of its own, no…
earakely-scale 6fb81c7
fix(gateway): a rate change starts at the instant it read the time; a…
earakely-scale 071322a
test(gateway): the unarmed clock's state carries its version and foll…
earakely-scale 70c1551
fix(gateway): a rate change keeps the clock's t0 and generation, and …
earakely-scale 261462b
refactor(gateway): every clock change through one path; set_rate its …
earakely-scale 0e832a7
Merge remote-tracking branch 'origin/main' into edgararakelyan/fd-547…
earakely-scale 5d049b7
fix(gateway): a wait or rate past a float's range is a 400, not a 500…
earakely-scale 26a87b3
refactor(gateway): a clock change's trigger wake read from its genera…
earakely-scale aaa2916
test(computer): the notes app's outside apt source is Docker's reposi…
earakely-scale 8dbc986
Merge branch 'main' into edgararakelyan/fd-5471-hb11-gateway-clock-on…
earakely-scale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """The followers of the gateway's clock (the computers on it): each long-polls ``/clock/watch`` with the version it | ||
| shows, which is its acknowledgement, and a clock write can wait until every one shows the write's version.""" | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import hmac | ||
| import math | ||
| import time | ||
| from collections.abc import Callable, Mapping | ||
|
|
||
| from .clock import Clock, ClockError | ||
| from .constants import CLOCK_MAX_WAIT_FOLLOWERS_S, CLOCK_WATCH_HOLD_S | ||
|
|
||
|
|
||
| def parse_wait(value) -> float: | ||
| """A write's ``wait_followers_s``: how long it waits for the followers, 0 when absent.""" | ||
| if value is None: | ||
| return 0.0 | ||
| if isinstance(value, bool) or not isinstance(value, (int, float)): | ||
| raise ClockError(f"wait_followers_s must be a non-negative number, got {value!r}") | ||
| # Only a float can be NaN or infinite; math.isfinite on an int past a float's range raises. | ||
| if (isinstance(value, float) and not math.isfinite(value)) or value < 0: | ||
| raise ClockError(f"wait_followers_s must be a non-negative number, got {value!r}") | ||
| if value > CLOCK_MAX_WAIT_FOLLOWERS_S: | ||
| raise ClockError(f"wait_followers_s exceeds max {CLOCK_MAX_WAIT_FOLLOWERS_S:g}") | ||
| return float(value) | ||
|
|
||
|
|
||
| class ClockFollowers: | ||
| """The declared followers, each by name with the read token its watch carries, and the version each last showed. | ||
| Its event and condition bind to the loop that first waits on them, so one gateway's watches and waiting writes all | ||
| run on the loop that serves it, never one asyncio.run apiece.""" | ||
|
|
||
| WATCH_S = CLOCK_WATCH_HOLD_S | ||
|
|
||
| def __init__(self, clock: Callable[[], Clock], tokens: Mapping[str, str]): | ||
| self._clock = clock | ||
| self._tokens = dict(tokens) | ||
| self._shown: dict[str, int | None] = dict.fromkeys(self._tokens) | ||
| self._seen: dict[str, float] = {} | ||
| self._changed = asyncio.Event() | ||
| self._acks = asyncio.Condition() | ||
|
|
||
| def changed(self) -> None: | ||
| """Answer every held watch: the clock has a new version.""" | ||
| self._changed.set() | ||
| self._changed = asyncio.Event() | ||
|
|
||
| def follower_for(self, name: str | None, token: str) -> str | None: | ||
| """``name`` when it's a declared follower and ``token`` its read token, else None.""" | ||
| expected = self._tokens.get(name or "") | ||
| return name if expected and token and hmac.compare_digest(token.encode(), expected.encode()) else None | ||
|
|
||
| async def watch(self, after: str, follower: str | None) -> None: | ||
| """Take ``after`` as ``follower``'s acknowledgement (one ``follower_for`` named) when it's a version this clock | ||
| has had, later than the one it last showed; then return at once when the clock's version isn't ``after``, else | ||
| at its next change or after ``WATCH_S``.""" | ||
| changed = self._changed | ||
| if follower is not None: | ||
| count = self._clock().count_of(after) | ||
| async with self._acks: | ||
| shown = self._shown[follower] | ||
| if count is not None and (shown is None or count > shown): | ||
| self._shown[follower] = count | ||
| self._seen[follower] = time.monotonic() | ||
| self._acks.notify_all() | ||
| if after == self._clock().version: | ||
| try: | ||
| await asyncio.wait_for(changed.wait(), self.WATCH_S) | ||
| except asyncio.TimeoutError: | ||
| pass | ||
|
|
||
| async def stale_after(self, version: str, wait_s: float) -> list[str]: | ||
| """The followers that haven't shown ``version`` (or a later one) once each has or ``wait_s`` has passed.""" | ||
| target = self._clock().count_of(version) | ||
|
|
||
| def stale() -> list[str]: | ||
| return sorted(name for name, shown in self._shown.items() | ||
| if shown is None or target is None or shown < target) | ||
|
|
||
| async with self._acks: | ||
| try: | ||
| await asyncio.wait_for(self._acks.wait_for(lambda: not stale()), wait_s) | ||
| except asyncio.TimeoutError: | ||
| pass | ||
| return stale() | ||
|
|
||
| def state(self) -> dict: | ||
| """Each follower's last shown version and how long ago it last watched, for ``/clock/state``.""" | ||
| now, clock = time.monotonic(), self._clock() | ||
| return {name: {"version": None if shown is None else clock.version_of(shown), | ||
| "seen_s_ago": round(now - self._seen[name], 3) if name in self._seen else None} | ||
| for name, shown in self._shown.items()} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClockFollowers.state()uses an inline3to choose the precision ofseen_s_ago. The repository requires magic numbers to be stored as class or instance variables with descriptive names. Move this value into a constant such asSEEN_SECONDS_DECIMALS. This repository requirement must be satisfied before merging.Rule Used: Store magic numbers as class or instance variables with descriptive names rather than using them inline in the code. (source)
Learned From
scaleapi/scaleapi#126388
Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!