doeff-time 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doeff_time-0.1.0/.gitignore +145 -0
- doeff_time-0.1.0/PKG-INFO +58 -0
- doeff_time-0.1.0/README.md +49 -0
- doeff_time-0.1.0/pyproject.toml +20 -0
- doeff_time-0.1.0/src/doeff_time/__init__.py +34 -0
- doeff_time-0.1.0/src/doeff_time/_internals/__init__.py +10 -0
- doeff_time-0.1.0/src/doeff_time/_internals/deadline.py +14 -0
- doeff_time-0.1.0/src/doeff_time/_internals/sim_clock.py +24 -0
- doeff_time-0.1.0/src/doeff_time/_internals/time_queue.py +68 -0
- doeff_time-0.1.0/src/doeff_time/_internals/validation.py +12 -0
- doeff_time-0.1.0/src/doeff_time/effects/__init__.py +29 -0
- doeff_time-0.1.0/src/doeff_time/effects/time.py +261 -0
- doeff_time-0.1.0/src/doeff_time/epoch_ms.py +21 -0
- doeff_time-0.1.0/src/doeff_time/handlers/__init__.py +13 -0
- doeff_time-0.1.0/src/doeff_time/handlers/_wall_ticks.py +59 -0
- doeff_time-0.1.0/src/doeff_time/handlers/async_time.py +142 -0
- doeff_time-0.1.0/src/doeff_time/handlers/sim_time.py +328 -0
- doeff_time-0.1.0/src/doeff_time/handlers/sync_time.py +127 -0
- doeff_time-0.1.0/src/doeff_time/py.typed +1 -0
- doeff_time-0.1.0/tests/conftest.py +38 -0
- doeff_time-0.1.0/tests/test_async_handler.py +93 -0
- doeff_time-0.1.0/tests/test_datetime_validation.py +30 -0
- doeff_time-0.1.0/tests/test_delay.py +15 -0
- doeff_time-0.1.0/tests/test_get_time.py +24 -0
- doeff_time-0.1.0/tests/test_race_within.py +122 -0
- doeff_time-0.1.0/tests/test_sim_close_out.py +74 -0
- doeff_time-0.1.0/tests/test_sim_daemon_pattern.py +237 -0
- doeff_time-0.1.0/tests/test_sim_many_services.py +212 -0
- doeff_time-0.1.0/tests/test_sim_migration_readiness.py +756 -0
- doeff_time-0.1.0/tests/test_sim_step_budget.py +147 -0
- doeff_time-0.1.0/tests/test_sim_time.py +356 -0
- doeff_time-0.1.0/tests/test_sim_time_effect_type_filter.py +98 -0
- doeff_time-0.1.0/tests/test_time_contract.hy +185 -0
- doeff_time-0.1.0/tests/test_time_queue.py +40 -0
- doeff_time-0.1.0/tests/test_wait_ticks.py +205 -0
- doeff_time-0.1.0/tests/test_wait_until.py +18 -0
- doeff_time-0.1.0/tests/test_wait_within.py +220 -0
- doeff_time-0.1.0/tests/time_contract_clocks.hy +68 -0
- doeff_time-0.1.0/tests/time_test_support.py +80 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
# macOS debug symbols for the compiled extension above: build output that
|
|
7
|
+
# embeds the builder's absolute paths, so it must not be distributed either.
|
|
8
|
+
*.dSYM/
|
|
9
|
+
.Python
|
|
10
|
+
env/
|
|
11
|
+
venv/
|
|
12
|
+
.venv/
|
|
13
|
+
ENV/
|
|
14
|
+
env.bak/
|
|
15
|
+
venv.bak/
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
|
|
19
|
+
# Distribution / packaging
|
|
20
|
+
.Python
|
|
21
|
+
build/
|
|
22
|
+
develop-eggs/
|
|
23
|
+
dist/
|
|
24
|
+
downloads/
|
|
25
|
+
eggs/
|
|
26
|
+
.eggs/
|
|
27
|
+
lib/
|
|
28
|
+
lib64/
|
|
29
|
+
parts/
|
|
30
|
+
sdist/
|
|
31
|
+
var/
|
|
32
|
+
wheels/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
.installed.cfg
|
|
35
|
+
*.egg
|
|
36
|
+
MANIFEST
|
|
37
|
+
|
|
38
|
+
# docs の site(make docs-site・mkdocs build の出力)
|
|
39
|
+
/site/
|
|
40
|
+
|
|
41
|
+
# Testing
|
|
42
|
+
.pytest_cache/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
.hypothesis/
|
|
48
|
+
htmlcov/
|
|
49
|
+
.tox/
|
|
50
|
+
|
|
51
|
+
# IDE
|
|
52
|
+
# Generated by Eclipse buildship and machine-specific: embeds the local Java home
|
|
53
|
+
# and toolchain paths. The gradle build does not read it.
|
|
54
|
+
ide-plugins/pycharm/.settings/org.eclipse.buildship.core.prefs
|
|
55
|
+
.idea/
|
|
56
|
+
.vscode/
|
|
57
|
+
*.swp
|
|
58
|
+
*.swo
|
|
59
|
+
*~
|
|
60
|
+
.DS_Store
|
|
61
|
+
|
|
62
|
+
# UV
|
|
63
|
+
!uv.lock
|
|
64
|
+
.venv/
|
|
65
|
+
|
|
66
|
+
# Ruff
|
|
67
|
+
.ruff_cache/
|
|
68
|
+
|
|
69
|
+
# MyPy
|
|
70
|
+
.mypy_cache/
|
|
71
|
+
.dmypy.json
|
|
72
|
+
dmypy.json
|
|
73
|
+
|
|
74
|
+
# Generated logs, traces, and local databases
|
|
75
|
+
.doeff-lint.jsonl
|
|
76
|
+
.doeff-flow/
|
|
77
|
+
*.jsonl
|
|
78
|
+
# 旧い版の coordinator の置き場の log(版上げの下見の検 test_deployed_state_upgrade.hy が読む fixture — 生成された log ではない)
|
|
79
|
+
!packages/doeff-cluster/tests/fixtures/coordinator_state_*/wal/wal.jsonl
|
|
80
|
+
# codex の app-server の実物の行を録った物(行の分類の検 packages/doeff-codex/tests/test_lines.hy が読む fixture — 生成された log ではない)
|
|
81
|
+
!packages/doeff-codex/tests/recorded/codex-*/*.jsonl
|
|
82
|
+
*.db
|
|
83
|
+
*.db-journal
|
|
84
|
+
*.db-shm
|
|
85
|
+
*.db-wal
|
|
86
|
+
*.sqlite
|
|
87
|
+
*.sqlite-journal
|
|
88
|
+
*.sqlite-shm
|
|
89
|
+
*.sqlite-wal
|
|
90
|
+
*.sqlite3
|
|
91
|
+
*.sqlite3-journal
|
|
92
|
+
*.sqlite3-shm
|
|
93
|
+
*.sqlite3-wal
|
|
94
|
+
None
|
|
95
|
+
Untitled
|
|
96
|
+
|
|
97
|
+
# Local agent and browser automation state
|
|
98
|
+
.agent-home/
|
|
99
|
+
.playwright-mcp/
|
|
100
|
+
.claude/settings.local.json
|
|
101
|
+
.claude/skills/install-vscode-plugin/
|
|
102
|
+
.agents/skills/install-vscode-plugin/
|
|
103
|
+
|
|
104
|
+
# Accidental desktop helper bundle captured at repository root
|
|
105
|
+
/server.js
|
|
106
|
+
|
|
107
|
+
# Rust build artifacts
|
|
108
|
+
rust/**/target/
|
|
109
|
+
packages/**/target/
|
|
110
|
+
|
|
111
|
+
# Local deploy keys
|
|
112
|
+
proboscis_doeff_deploy
|
|
113
|
+
proboscis_doeff_deploy.pub
|
|
114
|
+
|
|
115
|
+
# Worktrees and orchestration
|
|
116
|
+
.git-worktrees/
|
|
117
|
+
.orch/
|
|
118
|
+
.sisyphus/
|
|
119
|
+
VAULT
|
|
120
|
+
|
|
121
|
+
# Override global gitignore for examples
|
|
122
|
+
!examples/
|
|
123
|
+
ORCH_PROMPT.md
|
|
124
|
+
.worktrees/
|
|
125
|
+
|
|
126
|
+
# Local workspace artifacts
|
|
127
|
+
# Repo-root scratch directory only. A `python/` directory inside a package
|
|
128
|
+
# (e.g. packages/doeff-indexer/python/) is NOT covered by this rule.
|
|
129
|
+
/python/
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# 設計記録は repo に入れる — global の除外(`artifacts` の dir・`*.html`・`*.svg`)を打ち消す
|
|
133
|
+
# (打ち消しは dir を先に戻さないと中身に効かない)
|
|
134
|
+
!docs/design/*/artifacts/
|
|
135
|
+
!docs/design/**
|
|
136
|
+
# 書き込みロックだけは入れない(道具が作る一時 file — 打ち消しの後に置く)
|
|
137
|
+
docs/design/*/.records.lock
|
|
138
|
+
|
|
139
|
+
# Repository-owned Zenn article visuals (override global image ignores)
|
|
140
|
+
!/images/zenn-use-cases-v0/*.png
|
|
141
|
+
# 記事が参照する採用画像は generated/ の下に在る(上の行は直下だけに効き、2026-09-16 の 47 枚は入らなかった)
|
|
142
|
+
!/images/zenn-use-cases-v0/generated/
|
|
143
|
+
!/images/zenn-use-cases-v0/generated/*.png
|
|
144
|
+
# 手で書いた図の正本(global の `*.svg` の除外を打ち消す)
|
|
145
|
+
!/publications/zenn-use-cases-v0/visuals/*.svg
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: doeff-time
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic time effects for doeff
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: doeff-core-effects<0.9,>=0.8.0
|
|
7
|
+
Requires-Dist: doeff<0.9,>=0.8.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# doeff-time
|
|
11
|
+
|
|
12
|
+
Provider-agnostic time effects for `doeff`.
|
|
13
|
+
|
|
14
|
+
## Effects
|
|
15
|
+
|
|
16
|
+
- `Delay(seconds)`
|
|
17
|
+
- `WaitUntil(target: datetime)` (`target` must be timezone-aware)
|
|
18
|
+
- `GetTime()`
|
|
19
|
+
- `GetMonotonic()` — monotonic seconds (`float`); only differences are meaningful
|
|
20
|
+
- `ScheduleAt(time: datetime, program)` (`time` must be timezone-aware) — answers the spawned `Task`;
|
|
21
|
+
`Wait(task)` answers `program`'s value, or raises its failure
|
|
22
|
+
- `SetTime(time: datetime)` (for simulation handlers, `time` must be timezone-aware)
|
|
23
|
+
|
|
24
|
+
## Conversions
|
|
25
|
+
|
|
26
|
+
- `epoch_ms_of(at: datetime) -> int` — timezone-aware time → epoch milliseconds, floored by integer
|
|
27
|
+
`timedelta` division (no float on the way). This is the one rounding for every stamp that is laid
|
|
28
|
+
next to another stamp: two places rounding the same instant differently (round vs floor) make a
|
|
29
|
+
write at 329.6 ms look 1 ms later than a read at 329.9 ms.
|
|
30
|
+
|
|
31
|
+
## Handlers
|
|
32
|
+
|
|
33
|
+
- `async_time_handler()` for `asyncio` runtimes
|
|
34
|
+
- `sync_time_handler()` for blocking runtimes
|
|
35
|
+
- `sim_time_handler(start_time=...)` for deterministic virtual time (`start_time` is timezone-aware
|
|
36
|
+
`datetime`), or `sim_time_handler(clock=SimClock(start))` when the caller wants to read or move
|
|
37
|
+
the virtual clock outside the program (tests)
|
|
38
|
+
|
|
39
|
+
### Shared contract (all three handlers)
|
|
40
|
+
|
|
41
|
+
`tests/test_time_contract.hy` runs the same deftests under each handler (`:interpreters
|
|
42
|
+
["async" "sync" "sim"]`): readings never go back, `Delay(d)` / `WaitUntil(t)` advance the clock by
|
|
43
|
+
`d` / up to `t` (exactly on the virtual clock, with a small jitter bound on the wall clock), past
|
|
44
|
+
targets return at once, other effects pass through, and `ScheduleAt` tasks run in time order and
|
|
45
|
+
answer their program's value or failure through `Wait`. `SetTime` is simulation-only and stays out
|
|
46
|
+
of the shared contract. Note: the scheduled program runs under the handlers outside the time
|
|
47
|
+
handler, so it cannot itself perform time effects.
|
|
48
|
+
|
|
49
|
+
### Virtual time contract (`sim_time_handler`)
|
|
50
|
+
|
|
51
|
+
- Install inside `scheduled(...)` and outside every `Spawn` whose tasks sleep on it; spawned tasks
|
|
52
|
+
inherit the handler and share one clock.
|
|
53
|
+
- Time advances to the earliest pending wake-up only when no normal-priority task is runnable, so a
|
|
54
|
+
task woken at `t` reads `t` however many scheduler steps it takes.
|
|
55
|
+
- Wake-ups at the same instant resume in the order they were requested; the trace is identical on
|
|
56
|
+
the Rust scheduler (the default) and the Python one (`DOEFF_SCHEDULER=python`).
|
|
57
|
+
- The run ends when the root program returns, even while daemon services are still sleeping.
|
|
58
|
+
- `GetMonotonic()` answers the virtual clock's POSIX timestamp.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# doeff-time
|
|
2
|
+
|
|
3
|
+
Provider-agnostic time effects for `doeff`.
|
|
4
|
+
|
|
5
|
+
## Effects
|
|
6
|
+
|
|
7
|
+
- `Delay(seconds)`
|
|
8
|
+
- `WaitUntil(target: datetime)` (`target` must be timezone-aware)
|
|
9
|
+
- `GetTime()`
|
|
10
|
+
- `GetMonotonic()` — monotonic seconds (`float`); only differences are meaningful
|
|
11
|
+
- `ScheduleAt(time: datetime, program)` (`time` must be timezone-aware) — answers the spawned `Task`;
|
|
12
|
+
`Wait(task)` answers `program`'s value, or raises its failure
|
|
13
|
+
- `SetTime(time: datetime)` (for simulation handlers, `time` must be timezone-aware)
|
|
14
|
+
|
|
15
|
+
## Conversions
|
|
16
|
+
|
|
17
|
+
- `epoch_ms_of(at: datetime) -> int` — timezone-aware time → epoch milliseconds, floored by integer
|
|
18
|
+
`timedelta` division (no float on the way). This is the one rounding for every stamp that is laid
|
|
19
|
+
next to another stamp: two places rounding the same instant differently (round vs floor) make a
|
|
20
|
+
write at 329.6 ms look 1 ms later than a read at 329.9 ms.
|
|
21
|
+
|
|
22
|
+
## Handlers
|
|
23
|
+
|
|
24
|
+
- `async_time_handler()` for `asyncio` runtimes
|
|
25
|
+
- `sync_time_handler()` for blocking runtimes
|
|
26
|
+
- `sim_time_handler(start_time=...)` for deterministic virtual time (`start_time` is timezone-aware
|
|
27
|
+
`datetime`), or `sim_time_handler(clock=SimClock(start))` when the caller wants to read or move
|
|
28
|
+
the virtual clock outside the program (tests)
|
|
29
|
+
|
|
30
|
+
### Shared contract (all three handlers)
|
|
31
|
+
|
|
32
|
+
`tests/test_time_contract.hy` runs the same deftests under each handler (`:interpreters
|
|
33
|
+
["async" "sync" "sim"]`): readings never go back, `Delay(d)` / `WaitUntil(t)` advance the clock by
|
|
34
|
+
`d` / up to `t` (exactly on the virtual clock, with a small jitter bound on the wall clock), past
|
|
35
|
+
targets return at once, other effects pass through, and `ScheduleAt` tasks run in time order and
|
|
36
|
+
answer their program's value or failure through `Wait`. `SetTime` is simulation-only and stays out
|
|
37
|
+
of the shared contract. Note: the scheduled program runs under the handlers outside the time
|
|
38
|
+
handler, so it cannot itself perform time effects.
|
|
39
|
+
|
|
40
|
+
### Virtual time contract (`sim_time_handler`)
|
|
41
|
+
|
|
42
|
+
- Install inside `scheduled(...)` and outside every `Spawn` whose tasks sleep on it; spawned tasks
|
|
43
|
+
inherit the handler and share one clock.
|
|
44
|
+
- Time advances to the earliest pending wake-up only when no normal-priority task is runnable, so a
|
|
45
|
+
task woken at `t` reads `t` however many scheduler steps it takes.
|
|
46
|
+
- Wake-ups at the same instant resume in the order they were requested; the trace is identical on
|
|
47
|
+
the Rust scheduler (the default) and the Python one (`DOEFF_SCHEDULER=python`).
|
|
48
|
+
- The run ends when the root program returns, even while daemon services are still sleeping.
|
|
49
|
+
- `GetMonotonic()` answers the virtual clock's POSIX timestamp.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "doeff-time"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Provider-agnostic time effects for doeff"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"doeff>=0.8.0,<0.9",
|
|
13
|
+
"doeff-core-effects>=0.8.0,<0.9",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["src/doeff_time"]
|
|
18
|
+
|
|
19
|
+
[tool.uv.sources]
|
|
20
|
+
doeff = { workspace = true }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Public API for doeff-time."""
|
|
2
|
+
|
|
3
|
+
from doeff_time._internals.sim_clock import SimClock as SimClock
|
|
4
|
+
from doeff_time.effects.time import Delay as Delay
|
|
5
|
+
from doeff_time.effects.time import DelayEffect as DelayEffect
|
|
6
|
+
from doeff_time.effects.time import GetMonotonic as GetMonotonic
|
|
7
|
+
from doeff_time.effects.time import GetMonotonicEffect as GetMonotonicEffect
|
|
8
|
+
from doeff_time.effects.time import GetTime as GetTime
|
|
9
|
+
from doeff_time.effects.time import GetTimeEffect as GetTimeEffect
|
|
10
|
+
from doeff_time.effects.time import RaceOutcome as RaceOutcome
|
|
11
|
+
from doeff_time.effects.time import RaceWithin as RaceWithin
|
|
12
|
+
from doeff_time.effects.time import RaceWithinEffect as RaceWithinEffect
|
|
13
|
+
from doeff_time.effects.time import ScheduleAt as ScheduleAt
|
|
14
|
+
from doeff_time.effects.time import ScheduleAtEffect as ScheduleAtEffect
|
|
15
|
+
from doeff_time.effects.time import SetTime as SetTime
|
|
16
|
+
from doeff_time.effects.time import SetTimeEffect as SetTimeEffect
|
|
17
|
+
from doeff_time.effects.time import TicksOutcome as TicksOutcome
|
|
18
|
+
from doeff_time.effects.time import WaitTicks as WaitTicks
|
|
19
|
+
from doeff_time.effects.time import WaitTicksEffect as WaitTicksEffect
|
|
20
|
+
from doeff_time.effects.time import WaitUntil as WaitUntil
|
|
21
|
+
from doeff_time.effects.time import WaitUntilEffect as WaitUntilEffect
|
|
22
|
+
from doeff_time.effects.time import WaitWithin as WaitWithin
|
|
23
|
+
from doeff_time.effects.time import WaitWithinEffect as WaitWithinEffect
|
|
24
|
+
from doeff_time.effects.time import delay as delay
|
|
25
|
+
from doeff_time.effects.time import get_monotonic as get_monotonic
|
|
26
|
+
from doeff_time.effects.time import get_time as get_time
|
|
27
|
+
from doeff_time.effects.time import schedule_at as schedule_at
|
|
28
|
+
from doeff_time.effects.time import set_time as set_time
|
|
29
|
+
from doeff_time.effects.time import wait_until as wait_until
|
|
30
|
+
from doeff_time.effects.time import wait_within as wait_within
|
|
31
|
+
from doeff_time.epoch_ms import epoch_ms_of as epoch_ms_of
|
|
32
|
+
from doeff_time.handlers.async_time import async_time_handler as async_time_handler
|
|
33
|
+
from doeff_time.handlers.sim_time import sim_time_handler as sim_time_handler
|
|
34
|
+
from doeff_time.handlers.sync_time import sync_time_handler as sync_time_handler
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""The value a timed wait's deadline completes with."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class _DeadlinePassed:
|
|
5
|
+
"""The deadline of a timed wait passed. Told apart from every future's value by identity (a future may
|
|
6
|
+
complete with None — RaceWithin), so the clock handlers complete their deadline with this, never None."""
|
|
7
|
+
|
|
8
|
+
__slots__ = ()
|
|
9
|
+
|
|
10
|
+
def __repr__(self) -> str:
|
|
11
|
+
return "DEADLINE_PASSED"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEADLINE_PASSED = _DeadlinePassed()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Virtual monotonic clock for simulation handlers."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from .validation import ensure_aware_datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SimClock:
|
|
12
|
+
current_time: datetime = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
13
|
+
|
|
14
|
+
def __post_init__(self) -> None:
|
|
15
|
+
self.current_time = ensure_aware_datetime(self.current_time, name="current_time")
|
|
16
|
+
|
|
17
|
+
def advance_to(self, target_time: datetime) -> datetime:
|
|
18
|
+
target = ensure_aware_datetime(target_time, name="target_time")
|
|
19
|
+
self.current_time = max(self.current_time, target)
|
|
20
|
+
return self.current_time
|
|
21
|
+
|
|
22
|
+
def set_time(self, new_time: datetime) -> datetime:
|
|
23
|
+
self.current_time = ensure_aware_datetime(new_time, name="new_time")
|
|
24
|
+
return self.current_time
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Min-heap queue of time-ordered promises."""
|
|
2
|
+
|
|
3
|
+
import heapq
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from doeff_core_effects.scheduler import Promise
|
|
9
|
+
|
|
10
|
+
from doeff_time._internals.validation import ensure_aware_datetime
|
|
11
|
+
|
|
12
|
+
# ``Promise`` is generic to the type checker but not subscriptable at runtime.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TimeQueueEntry:
|
|
17
|
+
time: datetime
|
|
18
|
+
sequence: int
|
|
19
|
+
promise: "Promise[Any]"
|
|
20
|
+
# The value the clock completes ``promise`` with (a RaceWithin deadline's DEADLINE_PASSED, else None).
|
|
21
|
+
answer: Any = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TimeQueue:
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._mut_sequence = 0
|
|
27
|
+
self._mut_items: "list[tuple[datetime, int, Promise[Any], Any]]" = [] # noqa: UP037
|
|
28
|
+
# Sequences of entries withdrawn before their time (a timed wait whose future won —
|
|
29
|
+
# agora-redesign #2618). Dropped lazily when they reach the head, so withdrawing is O(1)
|
|
30
|
+
# and the clock never advances to a deadline nobody waits for.
|
|
31
|
+
self._mut_withdrawn: set[int] = set()
|
|
32
|
+
# Sequences still in the heap and not withdrawn — answers "is it still queued?" in O(1).
|
|
33
|
+
self._mut_queued: set[int] = set()
|
|
34
|
+
|
|
35
|
+
def push(self, time: datetime, promise: "Promise[Any]", answer: Any = None) -> int:
|
|
36
|
+
"""Queue ``promise`` to be completed with ``answer`` at ``time``; answers the entry's sequence (for ``withdraw``)."""
|
|
37
|
+
target_time = ensure_aware_datetime(time, name="time")
|
|
38
|
+
self._mut_sequence += 1
|
|
39
|
+
heapq.heappush(self._mut_items, (target_time, self._mut_sequence, promise, answer))
|
|
40
|
+
self._mut_queued.add(self._mut_sequence)
|
|
41
|
+
return self._mut_sequence
|
|
42
|
+
|
|
43
|
+
def withdraw(self, sequence: int) -> None:
|
|
44
|
+
"""Drop a queued entry before its time (no-op when it was already popped)."""
|
|
45
|
+
if sequence in self._mut_queued:
|
|
46
|
+
self._mut_queued.discard(sequence)
|
|
47
|
+
self._mut_withdrawn.add(sequence)
|
|
48
|
+
self._drop_withdrawn_head()
|
|
49
|
+
|
|
50
|
+
def _drop_withdrawn_head(self) -> None:
|
|
51
|
+
"""Keep the head a live entry, so ``pop`` / ``empty`` never see a withdrawn deadline."""
|
|
52
|
+
while self._mut_items and self._mut_items[0][1] in self._mut_withdrawn:
|
|
53
|
+
_, sequence, _, _ = heapq.heappop(self._mut_items)
|
|
54
|
+
self._mut_withdrawn.discard(sequence)
|
|
55
|
+
|
|
56
|
+
def pop(self) -> TimeQueueEntry:
|
|
57
|
+
self._drop_withdrawn_head()
|
|
58
|
+
time, sequence, promise, answer = heapq.heappop(self._mut_items)
|
|
59
|
+
self._mut_queued.discard(sequence)
|
|
60
|
+
self._drop_withdrawn_head()
|
|
61
|
+
return TimeQueueEntry(time=time, sequence=sequence, promise=promise, answer=answer)
|
|
62
|
+
|
|
63
|
+
def empty(self) -> bool:
|
|
64
|
+
self._drop_withdrawn_head()
|
|
65
|
+
return not self._mut_items
|
|
66
|
+
|
|
67
|
+
def __len__(self) -> int:
|
|
68
|
+
return len(self._mut_queued)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Internal validation helpers for doeff-time."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def ensure_aware_datetime(value: datetime, *, name: str) -> datetime:
|
|
8
|
+
if not isinstance(value, datetime):
|
|
9
|
+
raise TypeError(f"{name} must be datetime, got {type(value).__name__}")
|
|
10
|
+
if value.tzinfo is None:
|
|
11
|
+
raise ValueError(f"{name} must be timezone-aware datetime")
|
|
12
|
+
return value
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Public time effects for doeff-time."""
|
|
2
|
+
|
|
3
|
+
from doeff_time.effects.time import Delay as Delay
|
|
4
|
+
from doeff_time.effects.time import DelayEffect as DelayEffect
|
|
5
|
+
from doeff_time.effects.time import GetMonotonic as GetMonotonic
|
|
6
|
+
from doeff_time.effects.time import GetMonotonicEffect as GetMonotonicEffect
|
|
7
|
+
from doeff_time.effects.time import GetTime as GetTime
|
|
8
|
+
from doeff_time.effects.time import GetTimeEffect as GetTimeEffect
|
|
9
|
+
from doeff_time.effects.time import RaceOutcome as RaceOutcome
|
|
10
|
+
from doeff_time.effects.time import RaceWithin as RaceWithin
|
|
11
|
+
from doeff_time.effects.time import RaceWithinEffect as RaceWithinEffect
|
|
12
|
+
from doeff_time.effects.time import ScheduleAt as ScheduleAt
|
|
13
|
+
from doeff_time.effects.time import ScheduleAtEffect as ScheduleAtEffect
|
|
14
|
+
from doeff_time.effects.time import SetTime as SetTime
|
|
15
|
+
from doeff_time.effects.time import SetTimeEffect as SetTimeEffect
|
|
16
|
+
from doeff_time.effects.time import TicksOutcome as TicksOutcome
|
|
17
|
+
from doeff_time.effects.time import WaitTicks as WaitTicks
|
|
18
|
+
from doeff_time.effects.time import WaitTicksEffect as WaitTicksEffect
|
|
19
|
+
from doeff_time.effects.time import WaitUntil as WaitUntil
|
|
20
|
+
from doeff_time.effects.time import WaitUntilEffect as WaitUntilEffect
|
|
21
|
+
from doeff_time.effects.time import WaitWithin as WaitWithin
|
|
22
|
+
from doeff_time.effects.time import WaitWithinEffect as WaitWithinEffect
|
|
23
|
+
from doeff_time.effects.time import delay as delay
|
|
24
|
+
from doeff_time.effects.time import get_monotonic as get_monotonic
|
|
25
|
+
from doeff_time.effects.time import get_time as get_time
|
|
26
|
+
from doeff_time.effects.time import schedule_at as schedule_at
|
|
27
|
+
from doeff_time.effects.time import set_time as set_time
|
|
28
|
+
from doeff_time.effects.time import wait_until as wait_until
|
|
29
|
+
from doeff_time.effects.time import wait_within as wait_within
|