2025-03-13 23:17:04 +00:00
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
import trio
|
|
|
|
|
import pytest
|
2025-04-07 22:07:58 +00:00
|
|
|
|
2025-03-13 23:17:04 +00:00
|
|
|
import tractor
|
2026-04-17 23:16:16 +00:00
|
|
|
|
|
|
|
|
# XXX `cffi` dun build on py3.14 yet..
|
2026-06-17 21:16:36 +00:00
|
|
|
pytest.importorskip("cffi")
|
2026-04-17 23:16:16 +00:00
|
|
|
|
2025-04-07 22:07:58 +00:00
|
|
|
from tractor.ipc._ringbuf import (
|
2025-03-14 02:12:20 +00:00
|
|
|
open_ringbuf,
|
|
|
|
|
RBToken,
|
2025-03-13 23:17:04 +00:00
|
|
|
RingBuffSender,
|
|
|
|
|
RingBuffReceiver
|
|
|
|
|
)
|
2025-04-07 22:07:58 +00:00
|
|
|
from tractor._testing.samples import (
|
|
|
|
|
generate_sample_messages,
|
|
|
|
|
)
|
2025-03-13 23:17:04 +00:00
|
|
|
|
2026-04-17 23:16:16 +00:00
|
|
|
# XXX, in case you want to melt your cores, comment this skip line XD
|
2025-04-06 18:44:40 +00:00
|
|
|
pytestmark = pytest.mark.skip
|
|
|
|
|
|
2026-04-17 17:26:19 +00:00
|
|
|
# XXX `cffi` dun build on py3.14 yet..
|
|
|
|
|
cffi = pytest.importorskip("cffi")
|
|
|
|
|
|
2025-03-13 23:17:04 +00:00
|
|
|
|
|
|
|
|
@tractor.context
|
|
|
|
|
async def child_read_shm(
|
|
|
|
|
ctx: tractor.Context,
|
|
|
|
|
msg_amount: int,
|
2025-03-14 02:12:20 +00:00
|
|
|
token: RBToken,
|
2025-03-13 23:17:04 +00:00
|
|
|
total_bytes: int,
|
|
|
|
|
) -> None:
|
|
|
|
|
recvd_bytes = 0
|
|
|
|
|
await ctx.started()
|
|
|
|
|
start_ts = time.time()
|
2025-03-14 03:25:10 +00:00
|
|
|
async with RingBuffReceiver(token) as receiver:
|
2025-03-13 23:17:04 +00:00
|
|
|
while recvd_bytes < total_bytes:
|
|
|
|
|
msg = await receiver.receive_some()
|
|
|
|
|
recvd_bytes += len(msg)
|
|
|
|
|
|
|
|
|
|
# make sure we dont hold any memoryviews
|
|
|
|
|
# before the ctx manager aclose()
|
|
|
|
|
msg = None
|
|
|
|
|
|
|
|
|
|
end_ts = time.time()
|
|
|
|
|
elapsed = end_ts - start_ts
|
|
|
|
|
elapsed_ms = int(elapsed * 1000)
|
|
|
|
|
|
|
|
|
|
print(f'\n\telapsed ms: {elapsed_ms}')
|
|
|
|
|
print(f'\tmsg/sec: {int(msg_amount / elapsed):,}')
|
|
|
|
|
print(f'\tbytes/sec: {int(recvd_bytes / elapsed):,}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tractor.context
|
|
|
|
|
async def child_write_shm(
|
|
|
|
|
ctx: tractor.Context,
|
|
|
|
|
msg_amount: int,
|
|
|
|
|
rand_min: int,
|
|
|
|
|
rand_max: int,
|
2025-03-14 02:12:20 +00:00
|
|
|
token: RBToken,
|
2025-03-13 23:17:04 +00:00
|
|
|
) -> None:
|
|
|
|
|
msgs, total_bytes = generate_sample_messages(
|
|
|
|
|
msg_amount,
|
|
|
|
|
rand_min=rand_min,
|
|
|
|
|
rand_max=rand_max,
|
|
|
|
|
)
|
|
|
|
|
await ctx.started(total_bytes)
|
2025-03-14 03:25:10 +00:00
|
|
|
async with RingBuffSender(token) as sender:
|
2025-03-13 23:17:04 +00:00
|
|
|
for msg in msgs:
|
|
|
|
|
await sender.send_all(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
'msg_amount,rand_min,rand_max,buf_size',
|
|
|
|
|
[
|
|
|
|
|
# simple case, fixed payloads, large buffer
|
|
|
|
|
(100_000, 0, 0, 10 * 1024),
|
|
|
|
|
|
|
|
|
|
# guaranteed wrap around on every write
|
|
|
|
|
(100, 10 * 1024, 20 * 1024, 10 * 1024),
|
|
|
|
|
|
|
|
|
|
# large payload size, but large buffer
|
|
|
|
|
(10_000, 256 * 1024, 512 * 1024, 10 * 1024 * 1024)
|
|
|
|
|
],
|
|
|
|
|
ids=[
|
|
|
|
|
'fixed_payloads_large_buffer',
|
|
|
|
|
'wrap_around_every_write',
|
|
|
|
|
'large_payloads_large_buffer',
|
|
|
|
|
]
|
|
|
|
|
)
|
2025-03-14 01:43:02 +00:00
|
|
|
def test_ringbuf(
|
2025-03-13 23:17:04 +00:00
|
|
|
msg_amount: int,
|
|
|
|
|
rand_min: int,
|
|
|
|
|
rand_max: int,
|
|
|
|
|
buf_size: int
|
|
|
|
|
):
|
|
|
|
|
async def main():
|
2025-03-14 02:12:20 +00:00
|
|
|
with open_ringbuf(
|
|
|
|
|
'test_ringbuf',
|
|
|
|
|
buf_size=buf_size
|
|
|
|
|
) as token:
|
|
|
|
|
proc_kwargs = {
|
|
|
|
|
'pass_fds': (token.write_eventfd, token.wrap_eventfd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
common_kwargs = {
|
|
|
|
|
'msg_amount': msg_amount,
|
|
|
|
|
'token': token,
|
|
|
|
|
}
|
|
|
|
|
async with tractor.open_nursery() as an:
|
|
|
|
|
send_p = await an.start_actor(
|
|
|
|
|
'ring_sender',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
proc_kwargs=proc_kwargs
|
|
|
|
|
)
|
|
|
|
|
recv_p = await an.start_actor(
|
|
|
|
|
'ring_receiver',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
proc_kwargs=proc_kwargs
|
|
|
|
|
)
|
|
|
|
|
async with (
|
|
|
|
|
send_p.open_context(
|
|
|
|
|
child_write_shm,
|
|
|
|
|
rand_min=rand_min,
|
|
|
|
|
rand_max=rand_max,
|
|
|
|
|
**common_kwargs
|
|
|
|
|
) as (sctx, total_bytes),
|
|
|
|
|
recv_p.open_context(
|
|
|
|
|
child_read_shm,
|
|
|
|
|
**common_kwargs,
|
|
|
|
|
total_bytes=total_bytes,
|
Remove `run_in_actor()` + the ria reap cluster
The final excision of #477: with zero in-repo callers left (all
tests/examples/docs migrated to `to_actor.run()` et al) the
entire legacy one-shot machinery drops out,
- `runtime/_supervise.py`: `ActorNursery.run_in_actor()`, the
`._cancel_after_result_on_exit` portal-set and the
`_reap_ria_portals()` teardown-reaper (both its happy-path
block-exit call AND the error-path snapshot + 0.5s-bounded
collection) are deleted — one-shot result-waiting now lives
entirely in the caller's task via `to_actor.run()`, whose
enclosing cancel-scope bounds the wait by construction (the
correct-scoping fix for the unbounded-reap hang class; the
`d1fb4a1a` guard test now passes structurally).
- `runtime/_portal.py`: `Portal._submit_for_result()`,
`._expect_result_ctx`, `._final_result_msg/_pld`,
`.wait_for_result()` + the deprecated `.result()` alias are
gone — a `Portal` no longer has any "main result" notion.
NB `Context.wait_for_result()` is a different (very alive)
API and is untouched.
- `spawn/_spawn.py`: `exhaust_portal()` +
`cancel_on_completion()` (the reaper tasks) deleted; backend
comment sweeps in `_trio.py`/`_mp.py`.
- `_exceptions.py`: the `NoResult` sentinel dies with its lone
reader.
- `tests/test_ringbuf.py`: drop a daemon-portal `.result()`
call that was already a warn + `NoResult` no-op (the ctx-acm
exit does the real result-wait); unshadow the 2nd `sctx` as
`rctx`.
- comment/docstring x-ref sweeps: `msg/types.py`,
`_context.py`, `to_actor/`, `tests/test_to_actor.py`.
Gate: `test_to_actor test_spawning test_cancellation
test_infected_asyncio test_local test_rpc` = 81 passed,
3 xfailed on `trio`; +`test_ringbuf` = 70 passed, 3 skipped,
3 xfailed on `mp_spawn`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-07-06 16:52:29 +00:00
|
|
|
) as (rctx, _sent),
|
2025-03-14 02:12:20 +00:00
|
|
|
):
|
Remove `run_in_actor()` + the ria reap cluster
The final excision of #477: with zero in-repo callers left (all
tests/examples/docs migrated to `to_actor.run()` et al) the
entire legacy one-shot machinery drops out,
- `runtime/_supervise.py`: `ActorNursery.run_in_actor()`, the
`._cancel_after_result_on_exit` portal-set and the
`_reap_ria_portals()` teardown-reaper (both its happy-path
block-exit call AND the error-path snapshot + 0.5s-bounded
collection) are deleted — one-shot result-waiting now lives
entirely in the caller's task via `to_actor.run()`, whose
enclosing cancel-scope bounds the wait by construction (the
correct-scoping fix for the unbounded-reap hang class; the
`d1fb4a1a` guard test now passes structurally).
- `runtime/_portal.py`: `Portal._submit_for_result()`,
`._expect_result_ctx`, `._final_result_msg/_pld`,
`.wait_for_result()` + the deprecated `.result()` alias are
gone — a `Portal` no longer has any "main result" notion.
NB `Context.wait_for_result()` is a different (very alive)
API and is untouched.
- `spawn/_spawn.py`: `exhaust_portal()` +
`cancel_on_completion()` (the reaper tasks) deleted; backend
comment sweeps in `_trio.py`/`_mp.py`.
- `_exceptions.py`: the `NoResult` sentinel dies with its lone
reader.
- `tests/test_ringbuf.py`: drop a daemon-portal `.result()`
call that was already a warn + `NoResult` no-op (the ctx-acm
exit does the real result-wait); unshadow the 2nd `sctx` as
`rctx`.
- comment/docstring x-ref sweeps: `msg/types.py`,
`_context.py`, `to_actor/`, `tests/test_to_actor.py`.
Gate: `test_to_actor test_spawning test_cancellation
test_infected_asyncio test_local test_rpc` = 81 passed,
3 xfailed on `trio`; +`test_ringbuf` = 70 passed, 3 skipped,
3 xfailed on `mp_spawn`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-07-06 16:52:29 +00:00
|
|
|
# ctx-acm exits await each child task's
|
|
|
|
|
# `Return` (the prior `recv_p.result()` here
|
|
|
|
|
# was a daemon-portal no-op).
|
|
|
|
|
pass
|
2025-03-14 02:12:20 +00:00
|
|
|
|
|
|
|
|
await send_p.cancel_actor()
|
|
|
|
|
await recv_p.cancel_actor()
|
2025-03-13 23:17:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tractor.context
|
|
|
|
|
async def child_blocked_receiver(
|
|
|
|
|
ctx: tractor.Context,
|
2025-03-14 02:12:20 +00:00
|
|
|
token: RBToken
|
2025-03-13 23:17:04 +00:00
|
|
|
):
|
2025-03-14 02:12:20 +00:00
|
|
|
async with RingBuffReceiver(token) as receiver:
|
2025-03-13 23:17:04 +00:00
|
|
|
await ctx.started()
|
|
|
|
|
await receiver.receive_some()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ring_reader_cancel():
|
|
|
|
|
async def main():
|
2025-03-14 03:25:10 +00:00
|
|
|
with open_ringbuf('test_ring_cancel_reader') as token:
|
2025-03-13 23:17:04 +00:00
|
|
|
async with (
|
2025-03-14 02:12:20 +00:00
|
|
|
tractor.open_nursery() as an,
|
|
|
|
|
RingBuffSender(token) as _sender,
|
2025-03-13 23:17:04 +00:00
|
|
|
):
|
2025-03-14 02:12:20 +00:00
|
|
|
recv_p = await an.start_actor(
|
|
|
|
|
'ring_blocked_receiver',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
proc_kwargs={
|
|
|
|
|
'pass_fds': (token.write_eventfd, token.wrap_eventfd)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
async with (
|
|
|
|
|
recv_p.open_context(
|
|
|
|
|
child_blocked_receiver,
|
|
|
|
|
token=token
|
|
|
|
|
) as (sctx, _sent),
|
|
|
|
|
):
|
|
|
|
|
await trio.sleep(1)
|
|
|
|
|
await an.cancel()
|
2025-03-13 23:17:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
with pytest.raises(tractor._exceptions.ContextCancelled):
|
|
|
|
|
trio.run(main)
|
2025-03-14 03:25:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@tractor.context
|
|
|
|
|
async def child_blocked_sender(
|
|
|
|
|
ctx: tractor.Context,
|
|
|
|
|
token: RBToken
|
|
|
|
|
):
|
|
|
|
|
async with RingBuffSender(token) as sender:
|
|
|
|
|
await ctx.started()
|
|
|
|
|
await sender.send_all(b'this will wrap')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ring_sender_cancel():
|
|
|
|
|
async def main():
|
|
|
|
|
with open_ringbuf(
|
|
|
|
|
'test_ring_cancel_sender',
|
|
|
|
|
buf_size=1
|
|
|
|
|
) as token:
|
|
|
|
|
async with tractor.open_nursery() as an:
|
|
|
|
|
recv_p = await an.start_actor(
|
|
|
|
|
'ring_blocked_sender',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
proc_kwargs={
|
|
|
|
|
'pass_fds': (token.write_eventfd, token.wrap_eventfd)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
async with (
|
|
|
|
|
recv_p.open_context(
|
|
|
|
|
child_blocked_sender,
|
|
|
|
|
token=token
|
|
|
|
|
) as (sctx, _sent),
|
|
|
|
|
):
|
|
|
|
|
await trio.sleep(1)
|
|
|
|
|
await an.cancel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with pytest.raises(tractor._exceptions.ContextCancelled):
|
|
|
|
|
trio.run(main)
|