Upgrade from 0.3.0 to 0.4.0¶
0.4.0 upgrade
This guide covers the changes from 0.3.0 to published 0.4.0. The project retains Beta maturity and the documented platform limits. Check the changelog and use documentation matching the source revision or package you install.
Existing dataclass configurations, synchronous runs, result artifacts, assessments, sweeps, and Prometheus output remain available. You do not need to repeat the 0.2-to-0.3 dataclass migration. Review async customizations and error handling first; the new experiment and progress APIs are optional.
Update async lifecycle assumptions¶
| Application behavior | 0.3.0 | 0.4.0 |
|---|---|---|
await client.arun() |
Delegates to run() through an executor; basic runs can use direct native calls. |
Always uses an isolated Python/CFFI worker. |
Cancel arun() or aserve_once() |
Cancels the await without stopping its executor operation. | Stops the owned worker and waits for cleanup before propagating cancellation. |
Override run() or run_once() |
The corresponding async method invokes that override. | Async methods use their built-in isolated path. |
Keep basic synchronous native calls serialized within a process. Moving a direct
call into asyncio.to_thread() does not give it owned cancellation. Server.stop()
remains cooperative between tests; it does not interrupt an active listener.
Review code that treats cancellation or a timeout as an immediate return.
timeout= bounds worker execution, including startup, but cleanup and an active
application callback can extend the time until the await returns. An outer
asyncio.timeout() instead cancels the awaiting task and can translate its
cancellation into TimeoutError.
Successful cleanup allows CancelledError or the worker's TimeoutError to
propagate. If cleanup cannot be confirmed, IperfCleanupError takes precedence
and retains the original cause. A server remains unavailable until its worker
is reaped. Do not suppress this error and report successful cancellation.
Forced termination produces no partial Result and does not prove that native
C finalizers ran. See the execution contract
for cleanup bounds and Linux worker lifetime.
Also handle raised worker setup errors, including IperfError and
IperfLibraryError; some basic synchronous client setup failures instead return
failed results. Completed native failures still return Result(ok=False).
Handle both exceptions and unsuccessful results, and keep performance acceptance
separate from result.ok.
Preserve application customizations explicitly¶
If a subclass used run() for logging, validation, or postprocessing, move that
logic into an explicit async wrapper or implement the async override too:
import logging
from iperf3_lib import Client
logger = logging.getLogger(__name__)
class LoggedClient(Client):
async def arun(self, *, timeout=None, on_event=None):
result = await super().arun(timeout=timeout, on_event=on_event)
logger.info("Native run completed: ok=%s", result.ok)
return result
This preserves the built-in worker ownership and exception behavior. Keep a
separate run() override if synchronous callers need the same customization.
For a server, apply the same pattern to aserve_once() and await
super().aserve_once(...). Calling the old synchronous override in an executor
would retain its old cancellation limitation.
Choose new execution APIs deliberately¶
run_plan() remains synchronous and sequential. The new
arun_plan() accepts
the same prepared plan but returns PlanExecutionResult, not the synchronous
PlanResult. Its cancellation, timeout, and cleanup exceptions expose
partial_result; retain that history before propagating the exception.
PlanCancelledError inherits asyncio.CancelledError, so except Exception
does not catch it. The guide shows explicit exception handling.
Do not switch runners while leaving assess_plan(execution) unchanged:
assessment-v1 and sweep-v1 do not accept owned async histories. Completed result
artifacts remain available for analysis under a declared comparison method.
Use arun_concurrent_plan() only for explicitly bounded
independent cells. It requires worker and aggregate target-rate limits and
zero pauses; ordinary async plans remain sequential. Its overlap history must
be considered when choosing a baseline; reservations alone do not prove traffic.
run_adaptive_udp() is a separate synchronous exploratory
API for confirmed tested UDP rates, not an exact capacity estimate or an async
replacement for finite sweeps.
Keep archive families distinct¶
Select readers by both kind and schema_version. Module names below are
relative to iperf3_lib; schema numbers are not package versions.
| Saved evidence | kind, schema_version |
Reader |
|---|---|---|
| Result artifact | iperf3-lib.result, 1 |
artifacts.loads_artifact() |
| Assessment | iperf3-lib.assessment, 1 |
reports.loads_report() |
| Sweep | iperf3-lib.sweep, 1 |
sweep_reports.loads_sweep_report() |
| Sequential async execution | iperf3-lib.plan-execution, 2 |
plan_reports.loads_plan_report() |
| Concurrent execution | iperf3-lib.plan-execution, 3 |
concurrent_reports.loads_concurrent_report() |
| Adaptive UDP experiment | iperf3-lib.adaptive-udp, 1 |
adaptive_reports.loads_adaptive_udp_report() |
The three v1 formats published with 0.3.0 retain their meanings and remain readable. Do not renumber them or strip evidence to fit a different reader. Earlier unreleased plan-v2 readers reject newer exception records carrying events; upgrade those readers before consuming such reports. This is separate from migrating published 0.3.0 archives. Strict loaders validate saved evidence without running benchmarks.
Adopt typed progress only when needed¶
Existing on_event callbacks still receive the four-field NativeEvent with
kind, data, sequence, and wall-clock received_at_seconds. They do not
become LiveEvent callbacks.
For typed progress, use Client.events() or Server.events_once() inside
async with, then obtain the same operation's result with await stream.result().
Context exit handles early abandonment and awaits cleanup. Native end is not
wrapper completion; check the final result's status. Progress loss can coexist
with a complete result, while lost reconstruction evidence can make a result
incomplete. Preserve final results with the artifact API: live events are runtime
objects, not another archive schema. See the live-event guide
for ownership, terminal events, and capture diagnostics.