Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions wool/proto/wire.proto
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ message Nack {
service Worker {
rpc dispatch (stream Request) returns (stream Response);
rpc stop (StopRequest) returns (Void);
rpc idle (Void) returns (IdleTime);
rpc idle (Void) returns (Idle);
}

message Request {
Expand Down Expand Up @@ -136,7 +136,7 @@ message StopRequest {
float timeout = 1;
}

message IdleTime {
message Idle {
// Seconds the worker has been continuously idle: time since the
// in-flight task set last emptied (startup counts as empty).
// 0 while any task is in flight; measured on a monotonic clock.
Expand Down
4 changes: 2 additions & 2 deletions wool/src/wool/protocol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from wool.protocol._wire import ChainManifest as ChainManifest
from wool.protocol._wire import ChannelOptions as ChannelOptions
from wool.protocol._wire import ContextVar as ContextVar
from wool.protocol._wire import IdleTime as IdleTime
from wool.protocol._wire import Idle as Idle
from wool.protocol._wire import Message as Message
from wool.protocol._wire import Nack as Nack
from wool.protocol._wire import Request as Request
Expand All @@ -36,7 +36,7 @@
"ChainManifest",
"ChannelOptions",
"ContextVar",
"IdleTime",
"Idle",
"Message",
"Nack",
"Request",
Expand Down
4 changes: 2 additions & 2 deletions wool/src/wool/protocol/_wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from wool.protocol.wire_pb2 import ChainManifest
from wool.protocol.wire_pb2 import ChannelOptions
from wool.protocol.wire_pb2 import ContextVar
from wool.protocol.wire_pb2 import IdleTime
from wool.protocol.wire_pb2 import Idle
from wool.protocol.wire_pb2 import Message
from wool.protocol.wire_pb2 import Nack
from wool.protocol.wire_pb2 import Request
Expand Down Expand Up @@ -58,7 +58,7 @@ def __call__(servicer, server) -> None: ...
"ChainManifest",
"ChannelOptions",
"ContextVar",
"IdleTime",
"Idle",
"Message",
"Nack",
"Request",
Expand Down
17 changes: 8 additions & 9 deletions wool/src/wool/runtime/worker/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,21 +206,20 @@ class IdleUnavailable(WoolError):
"""Raised when a worker does not implement the idle RPC.

A worker that predates the idle capability answers the idle RPC
with gRPC ``UNIMPLEMENTED``; `WorkerConnection.idle_time`
translates that into this typed signal so a polling client can
detect an old worker and treat idle reporting as unavailable,
distinct from a transient hiccup or an unhealthy peer. It descends
from `wool.WoolError` — not `RpcError` — because an absent
capability is not an RPC-health fault, so ``except RpcError`` does
not catch it.
with gRPC ``UNIMPLEMENTED``; `WorkerConnection.idle` translates
that into this typed signal so a polling client can detect an old
worker and treat idle reporting as unavailable, distinct from a
transient hiccup or an unhealthy peer. It descends from
`wool.WoolError` — not `RpcError` — because an absent capability is
not an RPC-health fault, so ``except RpcError`` does not catch it.
"""


# public
class WorkerConnection:
"""Direct single-worker control surface over a pooled gRPC channel.

Exposes `dispatch` (task execution), `idle_time` (poll the worker's
Exposes `dispatch` (task execution), `idle` (poll the worker's
continuous idle duration), and `stop` (shut the remote worker
down). ``close`` is distinct: it releases this connection's local
pooled channel, whereas `stop` terminates the remote worker.
Expand Down Expand Up @@ -487,7 +486,7 @@ async def close(self):
if self._uds_key is not None:
await _channel_pool.expire(self._uds_key)

async def idle_time(self, *, timeout: float | None = None) -> float:
async def idle(self, *, timeout: float | None = None) -> float:
"""Query how long the remote worker has been continuously idle.

Returns the worker's reported continuous idle duration; see
Expand Down
6 changes: 3 additions & 3 deletions wool/src/wool/runtime/worker/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ async def idle(
self,
request: protocol.Void,
context: ServicerContext | None,
) -> protocol.IdleTime:
) -> protocol.Idle:
"""Report how long the worker has been continuously idle.

Idle is the number of seconds since the in-flight task set
Expand All @@ -600,13 +600,13 @@ async def idle(
:param context:
The `grpc.aio.ServicerContext` for this request.
:returns:
An `IdleTime` carrying the continuous idle duration in
An `Idle` carrying the continuous idle duration in
seconds.
"""
seconds = (
0.0 if self._idle_since is None else time.monotonic() - self._idle_since
)
return protocol.IdleTime(seconds=seconds)
return protocol.Idle(seconds=seconds)

@staticmethod
def _create_worker_loop(
Expand Down
26 changes: 13 additions & 13 deletions wool/tests/integration/test_worker_idle.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class TestWorkerIdleReporting:
"cred",
[CredentialType.INSECURE, CredentialType.MTLS, CredentialType.ONE_WAY],
)
async def test_idle_time_should_accrue_from_startup_over_the_real_wire(
async def test_idle_should_accrue_from_startup_over_the_real_wire(
self, credentials_map, retry_grpc_internal, cred
):
"""Test idle accrues from startup, over each transport.
Expand All @@ -133,9 +133,9 @@ async def body():
conn = connect()

# Act
first = await conn.idle_time()
first = await conn.idle()
await asyncio.sleep(0.1)
second = await conn.idle_time()
second = await conn.idle()

# Assert
assert first >= 0.0
Expand All @@ -145,7 +145,7 @@ async def body():
await retry_grpc_internal(body)

@pytest.mark.asyncio
async def test_idle_time_should_report_zero_while_a_task_is_in_flight(
async def test_idle_should_report_zero_while_a_task_is_in_flight(
self, credentials_map, retry_grpc_internal, tmp_path
):
"""Test idle is zero while a dispatched routine runs on the worker.
Expand Down Expand Up @@ -177,7 +177,7 @@ async def body():
)

# Act & assert
assert await conn.idle_time() == 0.0
assert await conn.idle() == 0.0
finally:
dispatch.cancel()
with contextlib.suppress(BaseException):
Expand All @@ -186,7 +186,7 @@ async def body():
await retry_grpc_internal(body)

@pytest.mark.asyncio
async def test_idle_time_should_reset_after_the_in_flight_set_drains(
async def test_idle_should_reset_after_the_in_flight_set_drains(
self, credentials_map, retry_grpc_internal, tmp_path
):
"""Test idle resets once the worker's in-flight set drains.
Expand All @@ -210,7 +210,7 @@ async def body():
):
conn = connect()
await asyncio.sleep(0.2)
before = await conn.idle_time()
before = await conn.idle()

# Act
sentinel = tmp_path / "drain.txt"
Expand All @@ -219,10 +219,10 @@ async def body():
# Wait for the docket-drain to be reflected (idle > 0),
# then confirm it counts from the drain, not from startup.
async def _reset():
return await conn.idle_time() > 0.0
return await conn.idle() > 0.0

await _poll_coro(_reset)
after = await conn.idle_time()
after = await conn.idle()

# Assert
assert before > 0.0
Expand All @@ -231,7 +231,7 @@ async def _reset():
await retry_grpc_internal(body)

@pytest.mark.asyncio
async def test_idle_time_should_raise_idle_unavailable_for_a_legacy_worker(
async def test_idle_should_raise_idle_unavailable_for_a_legacy_worker(
self, retry_grpc_internal
):
"""Test idle surfaces IdleUnavailable against a legacy worker.
Expand All @@ -257,7 +257,7 @@ async def body():
try:
# Act & assert
with pytest.raises(IdleUnavailable) as exc_info:
await conn.idle_time()
await conn.idle()
assert not isinstance(exc_info.value, RpcError)
finally:
await conn.close()
Expand Down Expand Up @@ -287,15 +287,15 @@ async def body():
# Arrange
async with _bare_worker(credentials_map[cred]) as (worker, connect):
conn = connect()
assert await conn.idle_time() >= 0.0
assert await conn.idle() >= 0.0

# Act
await conn.stop()

# Assert
async def _unreachable():
try:
await conn.idle_time()
await conn.idle()
return False
except TransientRpcError:
return True
Expand Down
22 changes: 11 additions & 11 deletions wool/tests/protocol/test_wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
EXPECTED_MESSAGE_EXPORTS = [
"Ack",
"ChainManifest",
"IdleTime",
"Idle",
"Message",
"Nack",
"Request",
Expand Down Expand Up @@ -273,38 +273,38 @@ def test_nack_without_exception(self):
parsed.ParseFromString(nack.SerializeToString())
assert parsed.HasField("exception") is False

def test_idle_time_fields(self):
"""Test IdleTime carries the idle duration in seconds.
def test_idle_fields(self):
"""Test Idle carries the idle duration in seconds.

Given:
A seconds value, and the default construction.
When:
An IdleTime message is constructed with and without a value.
An Idle message is constructed with and without a value.
Then:
The seconds field should hold the value and default to 0.0.
"""
# Arrange, act, & assert
assert protocol.IdleTime(seconds=42.5).seconds == 42.5
assert protocol.IdleTime().seconds == 0.0
assert protocol.Idle(seconds=42.5).seconds == 42.5
assert protocol.Idle().seconds == 0.0

@settings(max_examples=100)
@given(seconds=st.floats(width=64, allow_nan=False, allow_infinity=False))
def test_idle_time_roundtrip(self, seconds):
"""Test IdleTime.seconds survives the wire-format round-trip.
def test_idle_roundtrip(self, seconds):
"""Test Idle.seconds survives the wire-format round-trip.

Given:
Any finite double value for the idle duration.
When:
An IdleTime message is serialized and re-parsed.
An Idle message is serialized and re-parsed.
Then:
The seconds field should equal the original exactly — a
proto double round-trips losslessly.
"""
# Arrange
message = protocol.IdleTime(seconds=seconds)
message = protocol.Idle(seconds=seconds)

# Act
parsed = protocol.IdleTime()
parsed = protocol.Idle()
parsed.ParseFromString(message.SerializeToString())

# Assert
Expand Down
Loading
Loading