Keep recurring delays positive
A recurring native timer should always schedule its next poll with an explicitly positive interval. Do not derive a continuing poll from a separate deadline that may already be in the past. Current replay evidence showed that a zero-duration native timer could stop the callback loop while leaving the state controlled by that loop visible.
POLL_INTERVAL = 0.1
def _poll_current_state():
global _pendingPoll
_pendingPoll = None
if not _isStarted:
return
current = _read_current_state()
_apply_current_state(current)
# The polling clock is independent from any already-expired display deadline.
_pendingPoll = BigWorld.callback(POLL_INTERVAL, _poll_current_state)
- Use an expiration deadline to decide state, not to calculate the next recurring poll after that deadline has elapsed.
- Keep the pending handle owned and cancellable exactly like any other callback.
- Revalidate the owner and context on every iteration; a positive delay does not make a stale loop safe.
Teardown rules
- Cancel pending callbacks during every cleanup path.
- Clear callback handles after cancellation and after execution.
- Do not store live entity, arena, view, or model objects inside long-lived closures.
- Re-read
BigWorld.player()and the current owner inside the callback. - Treat replay seek, reconnect, and route change as invalidation points.
- Log repeated retry exhaustion so readiness bugs are visible.
Callback vs event
A callback is appropriate when the client does not expose a stable readiness signal or when a short deferral is required. Prefer an event or service notification when one exists, because events usually encode ownership and lifecycle more clearly than polling.
adisp sequences callbacks through deferred callers
In the core adisp module, calling an @adisp_async function returns a deferred caller. The decorated producer is invoked when that caller receives its continuation. An @adisp_process generator dispatches these callers synchronously and normally returns None; it does not return a future or cancellation handle, schedule a new tick, or move blocking work off the client thread. A synchronous producer callback can resume the generator in the same call stack.
Source excerpt from the core async wrapper:
def wrapper(*args, **kwargs):
def caller(callback):
kwargs[cbname] = cbwrapper(callback)
func(*args, **kwargs)
return caller
The default continuation accepts one result argument. The settings requester adapts its underlying two-argument response into a single continuation result. Its missing-player/settings branch logs and returns without calling that continuation, so awaiting a requester is not itself a guarantee of eventual completion. Recheck the operation's actual result, timeout and cleanup paths.
- Single versus batch: one yielded caller resumes with a scalar. A nonempty list or tuple dispatches callers in order and resumes with results in input order after all callbacks. Dispatch is not atomic: an escaping caller error can prevent later callers from starting.
- Batch shape: use a reusable sequence. An empty batch has no callback to trigger another step; a one-pass iterator is consumed by the dispatcher's counting pass before its dispatch loop.
- Completion ownership: each caller must invoke its continuation exactly once. The dispatcher decrements a shared counter without recording which slots already completed. Duplicate calls can advance the wrong step; missing calls have no dispatcher-level timeout.
Check the import before applying that contract. The GUI decorator module can call its producer directly when the recognized callback parameter is already supplied. Its process wrapper also owns the shared waiting-caption state. Core and GUI decorators are not interchangeable just because their names match.
These are version-sensitive Python sequencing contracts verified against current public source. Cancellation, deadlines, stale-owner rejection and unexpected-exception diagnostics belong to the operation and its owner. This is not a runtime timing or request-completion test; use the separate future/scope guide for wg_async behavior.
Client wg_async futures
The client executor advances decorated generators in the current call stack until they yield an
unresolved future. These private futures are single-consumer continuations: a second pending
then() overwrites the first callback, and completed results are consumed rather than
replayed. Await once, then publish through an owned cache or event if several consumers need the
value.
- A timeout completes the waiter with
TimeoutErrorand requests producer cancellation, but that works only when the producer installed a meaningful handler. await_callback()does not cancel the underlying callback API. A late callback can still arrive after the local waiter timed out.- Cancellation normally abandons the continuation instead of resuming an exception path. The owner must independently clear busy flags, handlers, views, and retained state.
- Apply at most one timeout to a future in the current implementation.
Scopes and synchronization primitives
AsyncScope weakly tracks async events, semaphores, and queues. The owner must strongly
retain both scope and primitives. Destroying a scope is terminal: pending promises can surface
BrokenPromiseError, and no primitive should be used afterward.
AsyncEventis level-triggered and releases all current waiters when set.AsyncSemaphorewakes acquirers FIFO; keep a non-negative count and balanced acquire/release ownership.AsyncQueuebuffers values or exceptions FIFO and abandons buffers and waiters at terminal destruction.
Retain cancellable work, distinguish timeout from owner teardown, clear the retained future in
finally, and reject late results against current context identity.
Native callback helpers have different ownership contracts
Inspect helpers/CallbackDelayer.py before replacing a hand-owned BigWorld.callback loop: its scheduling and cancellation contract differs from other native helpers.
| Helper | Pending-work key | Dispatch behavior |
|---|---|---|
CallbackDelayer | The callable; arguments are not part of identity | Removes the entry before calling the body. A non-negative, non-None return value schedules another iteration. |
CallbacksSetByID | An explicit logical ID | Sets the handle to None before calling the body; the ID remains present. The return value does not repeat the timer. |
Scheduling the same callable in CallbackDelayer replaces its pending timer and arguments. Fresh lambdas or partials are different keys. Retain the exact callable used for cancellation. Return None explicitly from a one-shot callback: False and 0 satisfy the repeat condition.
Do not both schedule the same callable inside its body and return a repeat delay: the wrapper can schedule a second timer and overwrite the tracked handle. Clearing pending work does not abort a body already executing; retain an owner/context guard. With CallbacksSetByID, hasDelayedCallbackID proves retained ID membership, not a live timer. Engine timing and cancellation races still need client testing.