Skip to content

Commit 5126a17

Browse files
authored
Make the timer generic and support periodic timers (#79)
The current timer provided is very convenient to implement timers, but it's not very helpful to implement a peridic timer that doesn't accumulate drift. This PR makes the timer configurable via *missing ticks policies* and adds 3 policies: `TriggerAllMissing`, `SkipMissingAndResync` and `SkipMissingAndDrift`. For convenience, 2 alternative constructors are provided for the most common cases: `Timer.timeout()` and `Timer.periodic()`. Timeout timers always drift and periodic timers never. The new `Timer` also uses the `asyncio` loop monotonic clock, and returns the `drift` (difference between when the timer should have triggered and it actually did) instead of a `datetime` with the time of trigger. It is also now possible to control when this timer is started, and can be stopped and reset/restarted at will. Fixes #78.
2 parents 35552b4 + 390439f commit 5126a17

File tree

8 files changed

+1239
-194
lines changed

8 files changed

+1239
-194
lines changed

.github/ISSUE_TEMPLATE/bug.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ body:
5050
- Build script, CI, dependencies, etc. (part:tooling)
5151
- Channels, `Broadcast`, `Bidirectional`, etc. (part:channels)
5252
- Select (part:select)
53-
- Utility receivers, `Merge`, `Timer`, etc. (part:receivers)
53+
- Utility receivers, `Merge`, etc. (part:receivers)
5454
validations:
5555
required: true
5656
- type: textarea

RELEASE_NOTES.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,41 @@
22

33
## Summary
44

5-
<!-- Here goes a general summary of what this release is about -->
5+
This release adds support to pass `None` values via channels and revamps the `Timer` class to support custom policies for handling missed ticks and use the loop monotonic clock.
66

77
## Upgrading
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including if there are any depractions and what they should be replaced with -->
9+
* `util.Timer` was replaced by a more generic implementation that allows for customizable policies to handle missed ticks.
10+
11+
If you were using `Timer` to implement timeouts, these two pices of code should be almost equivalent:
12+
13+
- Old:
14+
15+
```python
16+
old_timer = Timer(1.0)
17+
triggered_datetime = old_timer.receive()
18+
```
19+
20+
- New:
21+
22+
```python
23+
new_timer = Timer.timeout(timedelta(seconds=1.0))
24+
drift = new_timer.receive()
25+
triggered_datetime = datetime.now(timezone.utc) - drift
26+
```
27+
28+
They are not **exactly** the same because the `triggered_datetime` in the second case will not be exactly when the timer had triggered, but that shouldn't be relevant, the important part is when your code can actually react to the timer trigger and to know how much drift there was to be able to take corrective actions.
29+
30+
Also the new `Timer` uses the `asyncio` loop monotonic clock and the old one used the wall clock (`datetime.now()`) to track time. This means that when using `async-solipsism` to test, the new `Timer` will always trigger immediately regarless of the state of the wall clock.
31+
32+
**Note:** Before replacing this code blindly in all uses of `Timer.timeout()`, please consider using the periodic timer constructor `Timer.periodic()` if you need a timer that triggers reliable on a periodic fashion, as the old `Timer` (and `Timer.timeout()`) accumulates drift, which might not be what you want.
1033

1134
## New Features
1235

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
36+
* `util.Timer` was replaced by a more generic implementation that allows for customizable policies to handle missed ticks.
37+
38+
* Passing `None` values via channels is now supported.
1439

1540
## Bug Fixes
1641

17-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
42+
* `util.Select` / `util.Merge` / `util.MergeNamed`: Cancel pending tasks in `__del__` methods only if possible (the loop is not already closed).

noxfile.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,30 @@ def formatting(session: nox.Session) -> None:
2828
@nox.session
2929
def pylint(session: nox.Session) -> None:
3030
"""Run pylint to do lint checks."""
31-
session.install("-e", ".[docs]", "pylint", "pytest", "nox")
31+
session.install(
32+
"-e",
33+
".[docs]",
34+
"pylint",
35+
"pytest",
36+
"nox",
37+
"async-solipsism",
38+
"hypothesis",
39+
)
3240
session.run("pylint", *check_dirs, *check_files)
3341

3442

3543
@nox.session
3644
def mypy(session: nox.Session) -> None:
3745
"""Run mypy to check type hints."""
38-
session.install("-e", ".[docs]", "pytest", "nox", "mypy")
46+
session.install(
47+
"-e",
48+
".[docs]",
49+
"pytest",
50+
"nox",
51+
"mypy",
52+
"async-solipsism",
53+
"hypothesis",
54+
)
3955

4056
common_args = [
4157
"--namespace-packages",
@@ -59,7 +75,7 @@ def mypy(session: nox.Session) -> None:
5975
@nox.session
6076
def docstrings(session: nox.Session) -> None:
6177
"""Check docstring tone with pydocstyle and param descriptions with darglint."""
62-
session.install("pydocstyle", "darglint", "toml")
78+
session.install("pydocstyle", "darglint", "tomli")
6379

6480
session.run("pydocstyle", *check_dirs, *check_files)
6581

@@ -72,7 +88,14 @@ def docstrings(session: nox.Session) -> None:
7288
@nox.session
7389
def pytest(session: nox.Session) -> None:
7490
"""Run all tests using pytest."""
75-
session.install("pytest", "pytest-cov", "pytest-mock", "pytest-asyncio")
91+
session.install(
92+
"pytest",
93+
"pytest-cov",
94+
"pytest-mock",
95+
"pytest-asyncio",
96+
"async-solipsism",
97+
"hypothesis",
98+
)
7699
session.install("-e", ".")
77100
session.run(
78101
"pytest",

pyproject.toml

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[build-system]
22
requires = [
3-
"setuptools >= 65.3.0, < 66",
4-
"setuptools_scm[toml] >= 7.0.5, < 8",
5-
"wheel"
3+
"setuptools >= 65.3.0, < 66",
4+
"setuptools_scm[toml] >= 7.0.5, < 8",
5+
"wheel",
66
]
77
build-backend = "setuptools.build_meta"
88

@@ -12,36 +12,34 @@ name = "frequenz-channels"
1212
description = "Channel implementations for Python"
1313
readme = "README.md"
1414
license = { text = "MIT" }
15-
keywords = [ "frequenz", "channel" ]
15+
keywords = ["frequenz", "channel"]
1616
classifiers = [
17-
"Development Status :: 3 - Alpha",
18-
"Intended Audience :: Developers",
19-
"License :: OSI Approved :: MIT License",
20-
"Programming Language :: Python :: 3",
21-
"Programming Language :: Python :: 3 :: Only",
22-
"Programming Language :: Python :: 3.8",
23-
"Programming Language :: Python :: 3.9",
24-
"Programming Language :: Python :: 3.10",
25-
"Topic :: Software Development :: Libraries",
17+
"Development Status :: 3 - Alpha",
18+
"Intended Audience :: Developers",
19+
"License :: OSI Approved :: MIT License",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3 :: Only",
22+
"Programming Language :: Python :: 3.8",
23+
"Programming Language :: Python :: 3.9",
24+
"Programming Language :: Python :: 3.10",
25+
"Topic :: Software Development :: Libraries",
2626
]
2727
requires-python = ">= 3.8, < 4"
28-
dependencies = [
29-
"watchfiles >= 0.15.0",
30-
]
31-
dynamic = [ "version" ]
28+
dependencies = ["watchfiles >= 0.15.0"]
29+
dynamic = ["version"]
3230

3331
[[project.authors]]
34-
name ="Frequenz Energy-as-a-Service GmbH"
32+
name = "Frequenz Energy-as-a-Service GmbH"
3533
3634

3735
[project.optional-dependencies]
3836
docs = [
39-
"mike >= 1.1.2, < 2",
40-
"mkdocs-gen-files >= 0.4.0, < 0.5.0",
41-
"mkdocs-literate-nav >= 0.4.0, < 0.5.0",
42-
"mkdocs-material >= 8.5.7, < 9",
43-
"mkdocs-section-index >= 0.3.4, < 0.4.0",
44-
"mkdocstrings[python] >= 0.19.0, < 0.20.0",
37+
"mike >= 1.1.2, < 2",
38+
"mkdocs-gen-files >= 0.4.0, < 0.5.0",
39+
"mkdocs-literate-nav >= 0.4.0, < 0.5.0",
40+
"mkdocs-material >= 8.5.7, < 9",
41+
"mkdocs-section-index >= 0.3.4, < 0.4.0",
42+
"mkdocstrings[python] >= 0.19.0, < 0.20.0",
4543
]
4644

4745
[project.urls]
@@ -57,16 +55,16 @@ include-package-data = true
5755
version_scheme = "post-release"
5856

5957
[tool.pylint.similarities]
60-
ignore-comments=['yes']
61-
ignore-docstrings=['yes']
62-
ignore-imports=['no']
63-
min-similarity-lines=40
58+
ignore-comments = ['yes']
59+
ignore-docstrings = ['yes']
60+
ignore-imports = ['no']
61+
min-similarity-lines = 40
6462

6563
[tool.pylint.messages_control]
6664
# disable wrong-import-order, ungrouped-imports because it conflicts with isort
6765
disable = ["too-few-public-methods", "wrong-import-order", "ungrouped-imports"]
6866
[tool.pylint.'DESIGN']
69-
max-attributes=12
67+
max-attributes = 12
7068

7169
[tool.isort]
7270
profile = "black"
@@ -75,4 +73,8 @@ src_paths = ["src", "examples", "tests"]
7573

7674
[tool.pytest.ini_options]
7775
asyncio_mode = "auto"
78-
required_plugins = [ "pytest-asyncio", "pytest-mock" ]
76+
required_plugins = ["pytest-asyncio", "pytest-mock"]
77+
78+
[[tool.mypy.overrides]]
79+
module = ["async_solipsism", "async_solipsism.*"]
80+
ignore_missing_imports = true

src/frequenz/channels/util/__init__.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,34 @@
1717
multiple receivers into a single named stream, allowing to identify the
1818
origin of each message.
1919
20+
* [Timer][frequenz.channels.util.Timer]:
21+
A [receiver][frequenz.channels.Receiver] that ticks at certain intervals.
22+
2023
* [Select][frequenz.channels.util.Select]: A helper to select the next
2124
available message for each [receiver][frequenz.channels.Receiver] in a group
2225
of receivers.
23-
24-
* [Timer][frequenz.channels.util.Timer]:
25-
A [receiver][frequenz.channels.Receiver] that emits a *now* `timestamp`
26-
every `interval` seconds.
2726
"""
2827

2928
from ._file_watcher import FileWatcher
3029
from ._merge import Merge
3130
from ._merge_named import MergeNamed
3231
from ._select import Select
33-
from ._timer import Timer
32+
from ._timer import (
33+
MissedTickPolicy,
34+
SkipMissedAndDrift,
35+
SkipMissedAndResync,
36+
Timer,
37+
TriggerAllMissed,
38+
)
3439

3540
__all__ = [
3641
"FileWatcher",
3742
"Merge",
3843
"MergeNamed",
39-
"Select",
44+
"MissedTickPolicy",
4045
"Timer",
46+
"Select",
47+
"SkipMissedAndDrift",
48+
"SkipMissedAndResync",
49+
"TriggerAllMissed",
4150
]

0 commit comments

Comments
 (0)