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
19 changes: 14 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## PyTAK 7.5.1

- Treats a transient `EPERM` from a network send as a recoverable transport
outage. Linux can return this while firewall, route, or network namespace
state is being replaced during an appliance update. PyTAK now tears down the
affected client cleanly and reconnects with bounded backoff instead of
terminating the gateway process.

## PyTAK 7.5.0

- Added a common, additive runtime health contract to `StatusWriter`: `health`,
`input`, and `output` blocks describe process state, receiver activity, and
egress connection health without removing gateway-specific counters.

## PyTAK 7.4.3

- PyTAK-backed gateways now survive transient TAK transport failures in the
Expand Down Expand Up @@ -230,8 +244,3 @@ New Features:

Bug & Performance Fixes:
- Added async sleeps to each TX & RX loops iteration to fix broken async regiment in PYTAK.
# 7.5.0

* Add a common, additive runtime health contract to `StatusWriter`: `health`,
`input`, and `output` blocks describe process state, receiver activity, and
egress connection health without removing gateway-specific counters.
2 changes: 1 addition & 1 deletion src/pytak/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.5.0
7.5.1
5 changes: 5 additions & 0 deletions src/pytak/client_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,11 @@ def _retryable_transport_error(exc: Exception) -> bool:
errno.EHOSTUNREACH,
errno.ENETDOWN,
errno.ENETUNREACH,
# Linux can report EPERM from UDP sendto() while nftables,
# routes, or network namespaces are being replaced. Treat it as
# a transport outage so an appliance update cannot terminate a
# high-rate feeder during that short transition.
errno.EPERM,
errno.ETIMEDOUT,
}
if hasattr(errno, "EHOSTDOWN"):
Expand Down
37 changes: 37 additions & 0 deletions tests/test_client_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


import asyncio
import errno
import os

from configparser import ConfigParser, SectionProxy
Expand Down Expand Up @@ -564,6 +565,42 @@ async def test_run_with_reconnect_keeps_local_os_errors_fatal():
fake_sleep.assert_not_called()


def test_reconnect_keeps_local_access_denied_fatal():
"""Ordinary local file access failures remain configuration errors."""
error = PermissionError(errno.EACCES, "Permission denied")
assert not pytak.client_functions._retryable_transport_error(error)


@pytest.mark.asyncio
async def test_run_with_reconnect_retries_transient_udp_permission_error():
"""A firewall transition must not terminate a UDP-backed gateway."""
config_p = ConfigParser(
{
"PYTAK_RECONNECT_INITIAL": "1",
"PYTAK_RECONNECT_MAX": "2",
"PYTAK_RECONNECT_FACTOR": "2",
"PYTAK_RECONNECT_JITTER": "0",
"PYTAK_RECONNECT_RESET": "300",
}
)
config_p.add_section("fakeapp")
config = config_p["fakeapp"]
fake_main = AsyncMock(
side_effect=[PermissionError(errno.EPERM, "Operation not permitted"), None]
)
fake_sleep = AsyncMock()

with mock.patch(
"pytak.client_functions.main", new=fake_main
), mock.patch("pytak.client_functions.asyncio.sleep", new=fake_sleep):
await pytak.client_functions.run_with_reconnect(
"fakeapp", config, config_p
)

assert fake_main.call_count == 2
fake_sleep.assert_called_once_with(1)


@pytest.mark.asyncio
async def test_run_with_reconnect_retries_tak_enrollment_resolution():
"""A transient enrollment failure retries from the original deep link."""
Expand Down
Loading