Skip to content

Commit f1f44c3

Browse files
committed
Fix: Memory leak from upstream RouterOS-api, #321
1 parent 611e860 commit f1f44c3

2 files changed

Lines changed: 28 additions & 6 deletions

File tree

mktxp/flow/router_connection.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import functools
2121
import yaml
2222

23-
# Fix UTF-8 decode error
24-
# See: https://github.com/akpw/mktxp/issues/47
25-
# The RouterOS-api implicitly assumes that the API response is UTF-8 encoded.
26-
# But Mikrotik uses latin-1.
27-
# Because the upstream dependency is currently abandoned, this is a quick hack to solve the issue
23+
# Fix UTF-8 decode error and memory leak from upstream RouterOS-api
24+
(# Ref: https://github.com/akpw/mktxp/issues/47)
2825

26+
# 1. The RouterOS-api implicitly assumes that the API response is UTF-8 encoded,
27+
# but Mikrotik often uses latin-1, so monkey-patching StringField to fallback to latin-1
28+
29+
# 2. The upstream library uses `collections.defaultdict(StringField)` which caches every unique
30+
# key returned by the router, causing an unbounded memory leak. Monkey-patching it with LeaklessDefaultDict
2931
MIKROTIK_ENCODING = 'latin-1'
3032
import routeros_api.api_structure
3133

@@ -36,7 +38,13 @@ def _decode_bytes(bytes_to_decode):
3638
return bytes_to_decode.decode(MIKROTIK_ENCODING)
3739

3840
routeros_api.api_structure.StringField.get_python_value = lambda _, bytes: _decode_bytes(bytes)
39-
routeros_api.api_structure.default_structure = collections.defaultdict(routeros_api.api_structure.StringField)
41+
42+
class LeaklessDefaultDict(dict):
43+
def __missing__(self, key):
44+
return routeros_api.api_structure.StringField()
45+
46+
routeros_api.api_structure.default_structure = LeaklessDefaultDict()
47+
4048

4149
from routeros_api import RouterOsApiPool
4250

tests/flow/test_router_connection.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,17 @@ def test_router_connection_skips_ssl_context_building_when_ssl_disabled():
9191

9292
create_context.assert_not_called()
9393
assert routeros_pool.call_args.kwargs['ssl_context'] is None
94+
95+
96+
def test_leakless_default_dict_monkey_patch():
97+
# Verify the monkey patch is in place
98+
assert type(routeros_api.api_structure.default_structure).__name__ == 'LeaklessDefaultDict'
99+
100+
# Verify it returns a StringField for missing keys
101+
struct = routeros_api.api_structure.default_structure
102+
field = struct['some_random_key_123']
103+
assert isinstance(field, routeros_api.api_structure.StringField)
104+
105+
# Verify it does NOT cache the key, preventing the memory leak
106+
assert 'some_random_key_123' not in struct
107+
assert len(struct) == 0

0 commit comments

Comments
 (0)