ZaStoGram/Tools/mtproxy_phase_contract.py
loop-uh 8a5c466c82 Refactor MTProxy recorder and emoji animation scheduler
Extract MTProxy endpoint recording logic into MtProxyEndpointRecorder and MtProxyProbeLease helper classes, and introduce EmojiPanelAnimationScheduler for controlled animation playback in emoji panels.

Key changes:
- Move endpoint failure/handshake/data-path recording from ConnectionSocket to MtProxyEndpointRecorder
- Replace probe lease management methods with MtProxyProbeLease member class
- Add EmojiPanelAnimationScheduler to manage GIF/sticker playback based on visibility and scroll state
- Add animation FPS limiting and invalidation delegate hooks to ImageReceiver
- Update Java proxy event handling to route connected/connect_start/usable_success through ProxyEventReducer
- Update verification tools to reflect new architecture
2026-07-02 21:01:55 +03:00

338 lines
21 KiB
Python

#!/usr/bin/env python3
from dataclasses import dataclass
PHASE_LIVE = "live"
PHASE_FAILURE = "failure"
PHASE_SUCCESS = "success"
PHASE_NEUTRAL = "neutral"
ENDPOINT_EXACT = "exact"
ENDPOINT_NETWORK = "network"
ENDPOINT_NONE = "none"
EVIDENCE_NONE = "none"
EVIDENCE_PRE_TCP_LOCAL_WAIT = "pre_tcp_local_wait"
EVIDENCE_DNS_FAILURE = "dns_failure"
EVIDENCE_TCP_FAILURE = "tcp_failure"
EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO = "no_bytes_after_client_hello"
EVIDENCE_SERVER_BYTES_PARSER_FAILURE = "server_bytes_parser_failure"
EVIDENCE_SERVER_HELLO_HMAC_MISMATCH = "server_hello_hmac_mismatch"
EVIDENCE_POST_HANDSHAKE_NO_APP_DATA = "post_handshake_no_app_data"
EVIDENCE_CONFIG_INVALID_SECRET = "config_invalid_secret"
EVIDENCE_CANCELLED_OR_SHADOWED = "cancelled_or_shadowed"
EVIDENCE_CLASSES = {
EVIDENCE_NONE,
EVIDENCE_PRE_TCP_LOCAL_WAIT,
EVIDENCE_DNS_FAILURE,
EVIDENCE_TCP_FAILURE,
EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO,
EVIDENCE_SERVER_BYTES_PARSER_FAILURE,
EVIDENCE_SERVER_HELLO_HMAC_MISMATCH,
EVIDENCE_POST_HANDSHAKE_NO_APP_DATA,
EVIDENCE_CONFIG_INVALID_SECRET,
EVIDENCE_CANCELLED_OR_SHADOWED,
}
@dataclass(frozen=True)
class MtProxyPhase:
name: str
kind: str
native: bool
java: bool
analyzer: bool
reconnect_backoff: bool = False
endpoint_key: str = ENDPOINT_EXACT
rotation: bool = False
# Terminal verdict that can be assigned before the socket performs any
# real I/O. Must be preserved by deriveMtProxyTerminalDiagnostic across
# closeSocket (MtProxyPhase::isPreIoTerminalVerdict, generated into
# MtProxyPhaseClassification.h), otherwise the close path re-derives
# "connection_not_started" and neither endpoint cooldown nor reconnect
# backoff engages -> reconnect hot loop.
pre_io_terminal: bool = False
# Published through the native MtProxySocketObservation facade
# (mtProxySocketObservationIsHighRiskPhase): failures that must record an
# endpoint failure at close, plus the data-path success markers routed the
# same way.
observation_facade: bool = False
# Local scheduler/gate timeout: the wait was produced by our own pre-TCP
# machinery (admission queue, endpoint cooldown, gates), not by the
# network. MtProxyEndpointRecorder::recordFailure skips these so a local wake-up
# never counts as an endpoint failure. Deliberately EXCLUDES
# mtproxy_probe_wait_timeout (a probe that never came back is signal,
# see mtproxy-probe-hang history) and INCLUDES background_handshake_aborted
# (lifecycle cancellation, not endpoint evidence).
local_scheduler_timeout: bool = False
# Java-side ProxyPhasePolicy key scope when it deliberately differs from
# endpoint_key (empty = same). Two known divergences: waiting_tcp is a
# Java placeholder keyed by network, unknown_fail is keyed exact so an
# unattributed failure never poisons the whole network endpoint.
java_key_scope: str = ""
# Java scheduler ACTION_BACKOFF eligibility when it deliberately differs
# from the rotation flag (None = same as rotation). unknown_fail backs
# off (pace retries) without rotating (cannot attribute the failure).
java_backoff: "bool | None" = None
PHASES = (
MtProxyPhase("ok", PHASE_NEUTRAL, native=False, java=True, analyzer=True, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("checking", PHASE_NEUTRAL, native=False, java=True, analyzer=False, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("cancelled", PHASE_NEUTRAL, native=False, java=True, analyzer=False, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("unknown_fail", PHASE_FAILURE, native=False, java=True, analyzer=False, endpoint_key=ENDPOINT_NONE, java_key_scope=ENDPOINT_EXACT, java_backoff=True),
MtProxyPhase("admission_queue", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("endpoint_cooldown", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("tcp_connect_gate", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK),
MtProxyPhase("dns_coalesce_wait", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK),
MtProxyPhase("dns_cache_hit", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK),
MtProxyPhase("dns_cache_store", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK),
MtProxyPhase("mtproxy_probe_wait", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("phase_adaptive_recipe", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("secret_domain_sanitized", PHASE_LIVE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("host_resolve_start", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK),
MtProxyPhase("connect_start", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("socket_connect_start", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK),
MtProxyPhase("socket_connected", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK),
MtProxyPhase("client_hello_sent", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("admission_hold_after_client_hello_failure", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("server_hello_hmac_ok", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("on_connected", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("first_tls_app_sent", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("first_mtproxy_packet_sent", PHASE_LIVE, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("waiting_tcp", PHASE_LIVE, native=False, java=True, analyzer=False, endpoint_key=ENDPOINT_NONE, java_key_scope=ENDPOINT_NETWORK),
MtProxyPhase("first_tls_app_recv", PHASE_SUCCESS, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT, observation_facade=True),
MtProxyPhase("first_mtproxy_packet_recv", PHASE_SUCCESS, native=True, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT, observation_facade=True),
MtProxyPhase("start_failed", PHASE_FAILURE, native=False, java=True, analyzer=False, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("connection_not_started", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_NONE, local_scheduler_timeout=True),
MtProxyPhase("connecting_timeout", PHASE_FAILURE, native=False, java=True, analyzer=False, endpoint_key=ENDPOINT_EXACT, rotation=True),
MtProxyPhase("admission_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT, local_scheduler_timeout=True),
MtProxyPhase("endpoint_cooldown_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT, local_scheduler_timeout=True),
MtProxyPhase("mtproxy_probe_wait_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("dns_coalesce_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_NETWORK, local_scheduler_timeout=True),
MtProxyPhase("dns_negative_cache_hit", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_NETWORK, pre_io_terminal=True),
MtProxyPhase("dns_blocked_zero_address", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_NETWORK, pre_io_terminal=True, observation_facade=True),
MtProxyPhase("pre_tcp_gate_admission_overlap", PHASE_FAILURE, native=False, java=False, analyzer=True, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("tcp_budget_stolen_by_pre_tcp_wait", PHASE_FAILURE, native=False, java=False, analyzer=True, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("dns_budget_stolen_by_pre_tcp_wait", PHASE_FAILURE, native=False, java=False, analyzer=True, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("host_resolve_failed", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("host_resolve_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("tcp_connect_gate_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_NETWORK, local_scheduler_timeout=True),
MtProxyPhase("tcp_not_connected", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("tcp_connection_refused", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("tcp_connect_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("tcp_connected_no_pong", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("network_block_suspected", PHASE_FAILURE, native=False, java=True, analyzer=False, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("secret_parse_invalid_domain_control_char", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True, pre_io_terminal=True, observation_facade=True),
MtProxyPhase("secret_parse_invalid_domain", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True, pre_io_terminal=True, observation_facade=True),
MtProxyPhase("true_client_hello_timeout", PHASE_FAILURE, native=False, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("faketls_server_hello_wait_timeout", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("server_closed_after_client_hello", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("client_hello_sent_no_server_hello", PHASE_FAILURE, native=False, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("tls_alert_after_client_hello", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("short_tls_response_after_client_hello", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("unrecognized_response_after_client_hello", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("unrecognized_tls_response_after_client_hello", PHASE_FAILURE, native=False, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("server_hello_hmac_mismatch", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("faketls_not_mtproxy_response", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True, pre_io_terminal=True, observation_facade=True),
MtProxyPhase("faketls_no_server_hello_terminal", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True, pre_io_terminal=True, observation_facade=True),
MtProxyPhase("faketls_server_closed_terminal", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True, pre_io_terminal=True, observation_facade=True),
MtProxyPhase("background_handshake_aborted", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT, local_scheduler_timeout=True),
MtProxyPhase("handshake_profiles_exhausted", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True, pre_io_terminal=True, observation_facade=True),
MtProxyPhase("mtproxy_packet_sent_no_response", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True),
MtProxyPhase("post_handshake_no_appdata", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_EXACT, rotation=True, observation_facade=True),
MtProxyPhase("dropped_early_after_appdata", PHASE_FAILURE, native=True, java=True, analyzer=True, reconnect_backoff=True, endpoint_key=ENDPOINT_NETWORK, rotation=True),
MtProxyPhase("dropped_after_appdata", PHASE_FAILURE, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("connected_without_socket_connected_marker", PHASE_NEUTRAL, native=False, java=False, analyzer=True, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("handshake_ok_no_appdata_sent", PHASE_NEUTRAL, native=False, java=False, analyzer=True, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("shadowed_by_usable_success", PHASE_NEUTRAL, native=False, java=False, analyzer=True, endpoint_key=ENDPOINT_NONE),
MtProxyPhase("shadowed_socket_failure", PHASE_NEUTRAL, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("ignored_cancelled_generation", PHASE_NEUTRAL, native=True, java=True, analyzer=True, endpoint_key=ENDPOINT_EXACT),
MtProxyPhase("reconnect_backoff_suppressed", PHASE_NEUTRAL, native=True, java=False, analyzer=True, endpoint_key=ENDPOINT_NONE),
)
PHASE_EVIDENCE = {
"connection_not_started": EVIDENCE_PRE_TCP_LOCAL_WAIT,
"admission_timeout": EVIDENCE_PRE_TCP_LOCAL_WAIT,
"endpoint_cooldown_timeout": EVIDENCE_PRE_TCP_LOCAL_WAIT,
"mtproxy_probe_wait_timeout": EVIDENCE_PRE_TCP_LOCAL_WAIT,
"dns_coalesce_timeout": EVIDENCE_PRE_TCP_LOCAL_WAIT,
"tcp_connect_gate_timeout": EVIDENCE_PRE_TCP_LOCAL_WAIT,
"dns_negative_cache_hit": EVIDENCE_DNS_FAILURE,
"dns_blocked_zero_address": EVIDENCE_DNS_FAILURE,
"host_resolve_failed": EVIDENCE_DNS_FAILURE,
"host_resolve_timeout": EVIDENCE_DNS_FAILURE,
"tcp_not_connected": EVIDENCE_TCP_FAILURE,
"tcp_connection_refused": EVIDENCE_TCP_FAILURE,
"tcp_connect_timeout": EVIDENCE_TCP_FAILURE,
"tcp_connected_no_pong": EVIDENCE_TCP_FAILURE,
"faketls_server_hello_wait_timeout": EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO,
"true_client_hello_timeout": EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO,
"client_hello_sent_no_server_hello": EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO,
"tls_alert_after_client_hello": EVIDENCE_SERVER_BYTES_PARSER_FAILURE,
"short_tls_response_after_client_hello": EVIDENCE_SERVER_BYTES_PARSER_FAILURE,
"unrecognized_response_after_client_hello": EVIDENCE_SERVER_BYTES_PARSER_FAILURE,
"unrecognized_tls_response_after_client_hello": EVIDENCE_SERVER_BYTES_PARSER_FAILURE,
"server_hello_hmac_mismatch": EVIDENCE_SERVER_HELLO_HMAC_MISMATCH,
"faketls_not_mtproxy_response": EVIDENCE_SERVER_BYTES_PARSER_FAILURE,
"faketls_no_server_hello_terminal": EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO,
"faketls_server_closed_terminal": EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO,
"post_handshake_no_appdata": EVIDENCE_POST_HANDSHAKE_NO_APP_DATA,
"mtproxy_packet_sent_no_response": EVIDENCE_POST_HANDSHAKE_NO_APP_DATA,
"dropped_early_after_appdata": EVIDENCE_POST_HANDSHAKE_NO_APP_DATA,
"dropped_after_appdata": EVIDENCE_POST_HANDSHAKE_NO_APP_DATA,
"secret_parse_invalid_domain_control_char": EVIDENCE_CONFIG_INVALID_SECRET,
"secret_parse_invalid_domain": EVIDENCE_CONFIG_INVALID_SECRET,
"background_handshake_aborted": EVIDENCE_CANCELLED_OR_SHADOWED,
"shadowed_socket_failure": EVIDENCE_CANCELLED_OR_SHADOWED,
"ignored_cancelled_generation": EVIDENCE_CANCELLED_OR_SHADOWED,
}
def _validate_contract() -> None:
names = [phase.name for phase in PHASES]
duplicates = sorted({name for name in names if names.count(name) > 1})
if duplicates:
raise RuntimeError("duplicate MTProxy phase names: " + ", ".join(duplicates))
for phase in PHASES:
if phase.kind not in {PHASE_LIVE, PHASE_FAILURE, PHASE_SUCCESS, PHASE_NEUTRAL}:
raise RuntimeError(f"invalid phase kind for {phase.name}: {phase.kind}")
if phase.endpoint_key not in {ENDPOINT_EXACT, ENDPOINT_NETWORK, ENDPOINT_NONE}:
raise RuntimeError(f"invalid endpoint key mode for {phase.name}: {phase.endpoint_key}")
if not set(PHASE_EVIDENCE.values()) <= EVIDENCE_CLASSES:
raise RuntimeError("phase evidence mapping contains unknown evidence classes")
for phase in PHASES:
if phase.pre_io_terminal and not phase.native:
raise RuntimeError(
f"pre-I/O terminal phase {phase.name} must be a native phase: "
"it is preserved by deriveMtProxyTerminalDiagnostic in native code"
)
if phase.pre_io_terminal and phase.kind != PHASE_FAILURE:
raise RuntimeError(f"pre-I/O terminal phase {phase.name} must be a failure phase")
if phase.observation_facade and not phase.native:
raise RuntimeError(
f"observation facade phase {phase.name} must be a native phase: "
"the facade classifier is generated into native code"
)
if phase.local_scheduler_timeout and not phase.native:
raise RuntimeError(
f"local scheduler timeout phase {phase.name} must be a native phase: "
"the skip list is consumed by MtProxyEndpointRecorder::recordFailure in native code"
)
if phase.local_scheduler_timeout and phase.pre_io_terminal:
raise RuntimeError(
f"phase {phase.name} cannot be both local_scheduler_timeout and "
"pre_io_terminal: the skip list would swallow a terminal verdict"
)
def phases() -> tuple[MtProxyPhase, ...]:
return PHASES
def java_phase_names() -> set[str]:
return {phase.name for phase in PHASES if phase.java}
def native_phase_names() -> set[str]:
return {phase.name for phase in PHASES if phase.native}
def analyzer_phase_names() -> set[str]:
return {phase.name for phase in PHASES if phase.analyzer}
def analyzer_failure_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.analyzer and phase.kind == PHASE_FAILURE}
def evidence_classes() -> set[str]:
return set(EVIDENCE_CLASSES)
def evidence_for_phase(phase: str, response_bytes: int = 0) -> str:
if phase == "server_closed_after_client_hello":
return (
EVIDENCE_SERVER_BYTES_PARSER_FAILURE
if response_bytes > 0
else EVIDENCE_NO_BYTES_AFTER_CLIENT_HELLO
)
return PHASE_EVIDENCE.get(phase, EVIDENCE_NONE)
def java_visible_live_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.java and phase.kind in {PHASE_LIVE, PHASE_SUCCESS}}
def java_success_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.java and phase.kind == PHASE_SUCCESS}
def reconnect_backoff_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.reconnect_backoff}
def pre_io_terminal_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.pre_io_terminal}
def local_scheduler_timeout_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.local_scheduler_timeout}
def java_key_scope_for(phase: MtProxyPhase) -> str:
return phase.java_key_scope or phase.endpoint_key
def java_backoff_for(phase: MtProxyPhase) -> bool:
return phase.rotation if phase.java_backoff is None else phase.java_backoff
def java_backoff_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.java and java_backoff_for(phase)}
def java_rotation_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.java and phase.rotation}
def java_policy(name: str) -> tuple[str, str, bool, bool]:
"""(kind, key_scope, backoff, rotate) exactly as ProxyPhasePolicy.classify()
derives them from the generated table; checkers assert against this
instead of parsing the Java switch."""
for phase in PHASES:
if phase.name == name and phase.java:
if phase.kind == PHASE_NEUTRAL:
return (PHASE_NEUTRAL, ENDPOINT_NONE, False, False)
return (phase.kind, java_key_scope_for(phase), java_backoff_for(phase), phase.rotation)
# Unknown phases: ProxyPhasePolicy's conservative default.
return (PHASE_FAILURE, ENDPOINT_EXACT, True, False)
# Facade-only pseudo phases: published through the native observation facade
# but not part of the visible phase lifecycle table above.
FACADE_ONLY_PHASES = ("recipe_failed",)
def observation_facade_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.observation_facade} | set(FACADE_ONLY_PHASES)
def endpoint_key_phases(endpoint_key: str) -> set[str]:
return {
phase.name
for phase in PHASES
if phase.java and phase.kind == PHASE_FAILURE and phase.endpoint_key == endpoint_key
}
def rotation_phases() -> set[str]:
return {phase.name for phase in PHASES if phase.rotation}
_validate_contract()