cavisson-pythonagent 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
- cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
- cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
- cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
- cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
- pythonagent/__init__.py +22 -0
- pythonagent/agent/__init__.py +219 -0
- pythonagent/agent/internal/__init__.py +1 -0
- pythonagent/agent/internal/agent.py +1651 -0
- pythonagent/agent/internal/framesinfo.py +152 -0
- pythonagent/agent/internal/heap_dump.py +39 -0
- pythonagent/agent/internal/intercept_module.py +70 -0
- pythonagent/agent/internal/logs.py +122 -0
- pythonagent/agent/internal/metadata/__init__.py +0 -0
- pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
- pythonagent/agent/internal/proc_compat.py +75 -0
- pythonagent/agent/internal/profile.py +47 -0
- pythonagent/agent/internal/provider.py +83 -0
- pythonagent/agent/internal/thread_dump.py +85 -0
- pythonagent/agent/internal/udp.py +245 -0
- pythonagent/agent/internal/udp_message.py +800 -0
- pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
- pythonagent/agent/probes/Instrumentation/find.py +243 -0
- pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
- pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
- pythonagent/agent/probes/Instrumentation/parser.py +129 -0
- pythonagent/agent/probes/__init__.py +219 -0
- pythonagent/agent/probes/base.py +303 -0
- pythonagent/agent/probes/cache/__init__.py +51 -0
- pythonagent/agent/probes/cache/redis.py +119 -0
- pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
- pythonagent/agent/probes/coroutines/__init__.py +1 -0
- pythonagent/agent/probes/coroutines/asyncio.py +63 -0
- pythonagent/agent/probes/elasticdb/__init__.py +7 -0
- pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
- pythonagent/agent/probes/frameworks/__init__.py +27 -0
- pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
- pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
- pythonagent/agent/probes/frameworks/aisess.py +151 -0
- pythonagent/agent/probes/frameworks/asgi.py +340 -0
- pythonagent/agent/probes/frameworks/bottle.py +27 -0
- pythonagent/agent/probes/frameworks/cherry.py +25 -0
- pythonagent/agent/probes/frameworks/django.py +128 -0
- pythonagent/agent/probes/frameworks/falcon.py +21 -0
- pythonagent/agent/probes/frameworks/fastapi.py +35 -0
- pythonagent/agent/probes/frameworks/flask.py +30 -0
- pythonagent/agent/probes/frameworks/pyramid.py +56 -0
- pythonagent/agent/probes/frameworks/test.py +108 -0
- pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
- pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
- pythonagent/agent/probes/frameworks/wsgi.py +353 -0
- pythonagent/agent/probes/grpc/__init__.py +76 -0
- pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
- pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
- pythonagent/agent/probes/havoc/__init__.py +0 -0
- pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
- pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
- pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
- pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
- pythonagent/agent/probes/http/__init__.py +49 -0
- pythonagent/agent/probes/http/aiohttp_client.py +59 -0
- pythonagent/agent/probes/http/boto.py +12 -0
- pythonagent/agent/probes/http/httplib.py +110 -0
- pythonagent/agent/probes/http/httpx_client.py +116 -0
- pythonagent/agent/probes/http/requests.py +15 -0
- pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
- pythonagent/agent/probes/http/urllib3.py +16 -0
- pythonagent/agent/probes/langchain/__init__.py +21 -0
- pythonagent/agent/probes/langchain/base_tool.py +95 -0
- pythonagent/agent/probes/langchain/langchain_community.py +136 -0
- pythonagent/agent/probes/langchain/langchain_core.py +32 -0
- pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
- pythonagent/agent/probes/logging/__init__.py +106 -0
- pythonagent/agent/probes/message_brokers/__init__.py +4 -0
- pythonagent/agent/probes/message_brokers/pika.py +126 -0
- pythonagent/agent/probes/mongodb/__init__.py +6 -0
- pythonagent/agent/probes/mongodb/pymongo.py +286 -0
- pythonagent/agent/probes/openai/__init__.py +3 -0
- pythonagent/agent/probes/openai/openai.py +797 -0
- pythonagent/agent/probes/span.py +101 -0
- pythonagent/agent/probes/sql/__init__.py +13 -0
- pythonagent/agent/probes/sql/botocores3.py +51 -0
- pythonagent/agent/probes/sql/dbapi.py +285 -0
- pythonagent/agent/probes/sql/dynamodb.py +90 -0
- pythonagent/agent/probes/sql/mysql_connector.py +24 -0
- pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
- pythonagent/agent/probes/sql/mysqldb.py +43 -0
- pythonagent/agent/probes/sql/psycopg2.py +174 -0
- pythonagent/agent/probes/sql/pymysql.py +25 -0
- pythonagent/bootstrap/__init__.py +0 -0
- pythonagent/bootstrap/cav_gunicorn.py +26 -0
- pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
- pythonagent/bootstrap/run.py +47 -0
- pythonagent/bootstrap/sitecustomize.py +287 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
- pythonagent/config.py +279 -0
- pythonagent/find.py +72 -0
- pythonagent/find_mod_cls_name.py +54 -0
- pythonagent/lang.py +131 -0
- pythonagent/lib.py +91 -0
- pythonagent/main/__init__.py +0 -0
- pythonagent/main/pytrace/__init__.py +79 -0
- pythonagent/main/pytrace/commands/__init__.py +0 -0
- pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
- pythonagent/main/pytrace/commands/run.py +401 -0
- pythonagent/main/pytrace/pytrace.py +133 -0
- pythonagent/main/wsgi.py +6 -0
- pythonagent/main.py +27 -0
- pythonagent/run.py +46 -0
- pythonagent/sqins.py +8 -0
- pythonagent/test.py +73 -0
- pythonagent/utils.py +168 -0
- pythonagent/vendor/__init__.py +0 -0
- pythonagent/vendor/pympler/__init__.py +1 -0
- pythonagent/vendor/pympler/asizeof.py +2810 -0
- pythonagent/vendor/pympler/charts.py +62 -0
- pythonagent/vendor/pympler/classtracker.py +590 -0
- pythonagent/vendor/pympler/classtracker_stats.py +780 -0
- pythonagent/vendor/pympler/garbagegraph.py +80 -0
- pythonagent/vendor/pympler/mprofile.py +97 -0
- pythonagent/vendor/pympler/muppy.py +275 -0
- pythonagent/vendor/pympler/panels.py +115 -0
- pythonagent/vendor/pympler/process.py +238 -0
- pythonagent/vendor/pympler/py.typed +0 -0
- pythonagent/vendor/pympler/refbrowser.py +451 -0
- pythonagent/vendor/pympler/refgraph.py +350 -0
- pythonagent/vendor/pympler/summary.py +321 -0
- pythonagent/vendor/pympler/tracker.py +267 -0
- pythonagent/vendor/pympler/util/__init__.py +0 -0
- pythonagent/vendor/pympler/util/bottle.py +3809 -0
- pythonagent/vendor/pympler/util/compat.py +23 -0
- pythonagent/vendor/pympler/util/stringutils.py +77 -0
- pythonagent/vendor/pympler/web.py +346 -0
- pythonagent/vendor/werkzeug/__init__.py +20 -0
- pythonagent/vendor/werkzeug/_compat.py +228 -0
- pythonagent/vendor/werkzeug/_internal.py +473 -0
- pythonagent/vendor/werkzeug/_reloader.py +341 -0
- pythonagent/vendor/werkzeug/datastructures.py +3120 -0
- pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
- pythonagent/vendor/werkzeug/debug/console.py +218 -0
- pythonagent/vendor/werkzeug/debug/repr.py +297 -0
- pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
- pythonagent/vendor/werkzeug/exceptions.py +829 -0
- pythonagent/vendor/werkzeug/filesystem.py +64 -0
- pythonagent/vendor/werkzeug/formparser.py +584 -0
- pythonagent/vendor/werkzeug/http.py +1307 -0
- pythonagent/vendor/werkzeug/local.py +420 -0
- pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
- pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
- pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
- pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
- pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
- pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
- pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
- pythonagent/vendor/werkzeug/posixemulation.py +117 -0
- pythonagent/vendor/werkzeug/routing.py +2210 -0
- pythonagent/vendor/werkzeug/security.py +249 -0
- pythonagent/vendor/werkzeug/serving.py +1117 -0
- pythonagent/vendor/werkzeug/test.py +1123 -0
- pythonagent/vendor/werkzeug/testapp.py +241 -0
- pythonagent/vendor/werkzeug/urls.py +1138 -0
- pythonagent/vendor/werkzeug/useragents.py +202 -0
- pythonagent/vendor/werkzeug/utils.py +778 -0
- pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
- pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
- pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
- pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
- pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
- pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
- pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
- pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
- pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
- pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
- pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
- pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
- pythonagent/vendor/werkzeug/wsgi.py +1000 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Server-side gRPC interceptor.
|
|
2
|
+
|
|
3
|
+
intercept_service() synchronously receives the RpcMethodHandler for the
|
|
4
|
+
matched RPC and wraps its actual behavior callable directly - unlike Java's
|
|
5
|
+
ServerCall.Listener (registration now, completion callback later), so the BT
|
|
6
|
+
handle is captured once as a closure variable in intercept_service and
|
|
7
|
+
threaded explicitly into the wrapped callable, rather than relying on
|
|
8
|
+
ambient per-thread state. gRPC's own docs note interceptor and handler are
|
|
9
|
+
not guaranteed to run on the same thread; passing bt explicitly through the
|
|
10
|
+
closure makes this correct regardless (see DESIGN notes in the plan).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import grpc
|
|
16
|
+
|
|
17
|
+
from pythonagent.agent.probes.base import EntryPointInterceptor
|
|
18
|
+
from pythonagent.utils import get_current_thread_time_in_us
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NDGrpcServerInterceptor(EntryPointInterceptor, grpc.ServerInterceptor):
|
|
22
|
+
|
|
23
|
+
def __init__(self, agent):
|
|
24
|
+
super(NDGrpcServerInterceptor, self).__init__(agent, None)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def _extract_fp_instance(invocation_metadata):
|
|
28
|
+
if not invocation_metadata:
|
|
29
|
+
return None
|
|
30
|
+
for key, value in invocation_metadata:
|
|
31
|
+
if key and key.lower() == 'cavndfpinstance':
|
|
32
|
+
return value
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def _start(self, handler_call_details):
|
|
36
|
+
method = handler_call_details.method or ""
|
|
37
|
+
fp_instance = self._extract_fp_instance(getattr(handler_call_details, 'invocation_metadata', None))
|
|
38
|
+
cpu_time_before = get_current_thread_time_in_us()
|
|
39
|
+
bt = self.start_business_transaction(method, "", None, None, None, fp_instance)
|
|
40
|
+
return bt, cpu_time_before
|
|
41
|
+
|
|
42
|
+
def _finish(self, bt, cpu_time_before):
|
|
43
|
+
with self.log_exceptions():
|
|
44
|
+
cpu_time_after = get_current_thread_time_in_us()
|
|
45
|
+
self.end_business_transaction(bt, cpu_time_after - cpu_time_before)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _wrap_context_status(context):
|
|
49
|
+
"""Capture the status the moment the handler sets it, rather than
|
|
50
|
+
reading it back afterward (ServicerContext.code() is EXPERIMENTAL).
|
|
51
|
+
Returns a callable that yields the captured status (defaulting to OK).
|
|
52
|
+
"""
|
|
53
|
+
status_holder = {'code': grpc.StatusCode.OK}
|
|
54
|
+
real_set_code = context.set_code
|
|
55
|
+
real_abort = context.abort
|
|
56
|
+
|
|
57
|
+
def set_code(code, *args, **kwargs):
|
|
58
|
+
status_holder['code'] = code
|
|
59
|
+
return real_set_code(code, *args, **kwargs)
|
|
60
|
+
|
|
61
|
+
def abort(code, details=None, *args, **kwargs):
|
|
62
|
+
status_holder['code'] = code
|
|
63
|
+
return real_abort(code, details, *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
context.set_code = set_code
|
|
66
|
+
context.abort = abort
|
|
67
|
+
|
|
68
|
+
def get_status_code():
|
|
69
|
+
code = status_holder['code']
|
|
70
|
+
return 200 if (code is None or code == grpc.StatusCode.OK) else 500
|
|
71
|
+
|
|
72
|
+
return get_status_code
|
|
73
|
+
|
|
74
|
+
def _handle_exception(self, bt):
|
|
75
|
+
with self.log_exceptions():
|
|
76
|
+
if bt:
|
|
77
|
+
bt.add_exception(*sys.exc_info())
|
|
78
|
+
|
|
79
|
+
def intercept_service(self, continuation, handler_call_details):
|
|
80
|
+
handler = continuation(handler_call_details)
|
|
81
|
+
if handler is None:
|
|
82
|
+
return handler
|
|
83
|
+
|
|
84
|
+
if handler.request_streaming and handler.response_streaming:
|
|
85
|
+
behavior = self._wrap_streaming(handler.stream_stream, handler_call_details)
|
|
86
|
+
return grpc.stream_stream_rpc_method_handler(
|
|
87
|
+
behavior, handler.request_deserializer, handler.response_serializer)
|
|
88
|
+
if handler.request_streaming and not handler.response_streaming:
|
|
89
|
+
behavior = self._wrap_unary(handler.stream_unary, handler_call_details)
|
|
90
|
+
return grpc.stream_unary_rpc_method_handler(
|
|
91
|
+
behavior, handler.request_deserializer, handler.response_serializer)
|
|
92
|
+
if not handler.request_streaming and handler.response_streaming:
|
|
93
|
+
behavior = self._wrap_streaming(handler.unary_stream, handler_call_details)
|
|
94
|
+
return grpc.unary_stream_rpc_method_handler(
|
|
95
|
+
behavior, handler.request_deserializer, handler.response_serializer)
|
|
96
|
+
|
|
97
|
+
behavior = self._wrap_unary(handler.unary_unary, handler_call_details)
|
|
98
|
+
return grpc.unary_unary_rpc_method_handler(
|
|
99
|
+
behavior, handler.request_deserializer, handler.response_serializer)
|
|
100
|
+
|
|
101
|
+
def _wrap_unary(self, real_behavior, handler_call_details):
|
|
102
|
+
def wrapped(request_or_iterator, context):
|
|
103
|
+
bt, cpu_time_before = self._start(handler_call_details)
|
|
104
|
+
get_status_code = self._wrap_context_status(context)
|
|
105
|
+
try:
|
|
106
|
+
response = real_behavior(request_or_iterator, context)
|
|
107
|
+
except Exception:
|
|
108
|
+
self._handle_exception(bt)
|
|
109
|
+
self._finish(bt, cpu_time_before)
|
|
110
|
+
raise
|
|
111
|
+
self.handle_http_status_code(bt, get_status_code(), "")
|
|
112
|
+
self._finish(bt, cpu_time_before)
|
|
113
|
+
return response
|
|
114
|
+
return wrapped
|
|
115
|
+
|
|
116
|
+
def _wrap_streaming(self, real_behavior, handler_call_details):
|
|
117
|
+
def wrapped(request_or_iterator, context):
|
|
118
|
+
bt, cpu_time_before = self._start(handler_call_details)
|
|
119
|
+
get_status_code = self._wrap_context_status(context)
|
|
120
|
+
try:
|
|
121
|
+
for response in real_behavior(request_or_iterator, context):
|
|
122
|
+
yield response
|
|
123
|
+
except Exception:
|
|
124
|
+
self._handle_exception(bt)
|
|
125
|
+
self._finish(bt, cpu_time_before)
|
|
126
|
+
raise
|
|
127
|
+
self.handle_http_status_code(bt, get_status_code(), "")
|
|
128
|
+
self._finish(bt, cpu_time_before)
|
|
129
|
+
return wrapped
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Allocates heap memory by creating lists of Python float objects,
|
|
3
|
+
stressing the CPython heap allocator and cyclic garbage collector.
|
|
4
|
+
|
|
5
|
+
Why floats and not bytearray?
|
|
6
|
+
• bytearray(n) → single C malloc, NOT tracked by the GC, zero heap object pressure
|
|
7
|
+
• [i+0.1 for i in ...] → n individual PyFloatObject allocations, each ~24 bytes,
|
|
8
|
+
all tracked by the cyclic GC — true heap pressure
|
|
9
|
+
• array.array('d', ...) → like bytearray: one C buffer, no GC objects
|
|
10
|
+
|
|
11
|
+
Sizing math:
|
|
12
|
+
Each PyFloatObject = 24 bytes on 64-bit CPython (ob_refcnt + ob_type + ob_fval)
|
|
13
|
+
Plus list slot pointer = 8 bytes → ~32 bytes effective cost per element
|
|
14
|
+
count = ceil(objectSizeInBytes / 32)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
import math
|
|
19
|
+
import logging
|
|
20
|
+
import gc
|
|
21
|
+
import threading
|
|
22
|
+
from pythonagent.agent.internal.proc_compat import get_virtual_memory, get_free_memory_mb
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger('pythonagent.agent')
|
|
25
|
+
|
|
26
|
+
MEGA = 2 ** 20
|
|
27
|
+
GIGA = 2 ** 30
|
|
28
|
+
|
|
29
|
+
# CPython PyFloatObject size on 64-bit: ob_refcnt(8) + ob_type*(8) + ob_fval(8) = 24 bytes
|
|
30
|
+
# list slot (pointer to object) adds 8 bytes → ~32 bytes effective per element
|
|
31
|
+
_FLOAT_OBJECT_BYTES = 32
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Module-level so a stop request (from havoc_manager.parse_nethavoc_config) can
|
|
35
|
+
# reach a memory-leak havoc that's currently running in its own background thread.
|
|
36
|
+
_stop_event = threading.Event()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def stop_memory_leak():
|
|
40
|
+
"""Signal any running memory-leak havoc to stop immediately."""
|
|
41
|
+
logger.info("MemoryLeakHavoc: stop_memory_leak called")
|
|
42
|
+
_stop_event.set()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MemoryLeakObject:
|
|
46
|
+
"""
|
|
47
|
+
Allocates a list of Python floats to consume approximately
|
|
48
|
+
`object_size_in_bytes` of heap memory via PyFloatObject instances.
|
|
49
|
+
|
|
50
|
+
i + 0.1 is used to:
|
|
51
|
+
1. Prevent CPython's small-int cache from de-duplicating values
|
|
52
|
+
2. Ensure every element is a distinct PyFloatObject on the heap
|
|
53
|
+
(integers < 256 are cached singletons; floats never are)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
__slots__ = ('numbers',)
|
|
57
|
+
|
|
58
|
+
def __init__(self, object_size_in_bytes: int):
|
|
59
|
+
count = max(1, math.ceil(object_size_in_bytes / _FLOAT_OBJECT_BYTES))
|
|
60
|
+
# Each `i + 0.1` produces a unique PyFloatObject allocated on the heap.
|
|
61
|
+
# Python floats have NO intern/cache pool — every one is a fresh
|
|
62
|
+
# allocation, forcing pymalloc to work.
|
|
63
|
+
self.numbers: list = [i + 0.1 for i in range(count)]
|
|
64
|
+
|
|
65
|
+
def clear(self) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Release the reference to the float list.
|
|
68
|
+
Once all MemoryLeakObject references are dropped and GC runs,
|
|
69
|
+
the memory becomes eligible for reclamation.
|
|
70
|
+
"""
|
|
71
|
+
self.numbers = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def apply_memory_leak(leaksINMB: float, shootUpDurationInSec: float,
|
|
75
|
+
totalDurationInSec: float, objectSizeINKB: float) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Entry point — signature matches custom_memory_stress.apply_memory_leak
|
|
78
|
+
so havoc_manager.py needs zero changes.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
# Start the memory-leak havoc request with the requested pressure target.
|
|
82
|
+
logger.info("MemoryLeakHavoc: preparing leak request target=%.1fMB", leaksINMB)
|
|
83
|
+
free_mb = get_free_memory_mb()
|
|
84
|
+
if free_mb < leaksINMB:
|
|
85
|
+
logger.error(
|
|
86
|
+
"MemoryLeakHavoc: Memory havoc aborted: requested %.1fMB but only %.1fMB free",
|
|
87
|
+
leaksINMB, free_mb
|
|
88
|
+
)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Reset any stop signal left over from a previous run so this run starts clean.
|
|
92
|
+
_stop_event.clear()
|
|
93
|
+
|
|
94
|
+
logger.warning(
|
|
95
|
+
"MemoryLeakHavoc: Heap Pressure Havoc starting: target=%sMB chunkSize=%sKB "
|
|
96
|
+
"shootUp=%ss total=%ss",
|
|
97
|
+
leaksINMB, objectSizeINKB, shootUpDurationInSec, totalDurationInSec
|
|
98
|
+
)
|
|
99
|
+
_run_heap_pressure(leaksINMB, shootUpDurationInSec, totalDurationInSec, objectSizeINKB)
|
|
100
|
+
except Exception:
|
|
101
|
+
logger.exception("MemoryLeakHavoc: Failed while applying memory leak havoc")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _run_heap_pressure(leaksINMB: float, shootUpDurationInSec: float,
|
|
105
|
+
totalDurationInSec: float, objectSizeINKB: float) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Core ramp-up loop:
|
|
108
|
+
|
|
109
|
+
objectCount = ceil(totalBytes / objectSizeBytes)
|
|
110
|
+
delayPerObj = floor(rampUpMs / objectCount) [seconds here]
|
|
111
|
+
|
|
112
|
+
All MemoryLeakObject instances are retained in `memory_leaks` so the GC
|
|
113
|
+
cannot reclaim them during the hold phase.
|
|
114
|
+
"""
|
|
115
|
+
memory_leaks: list = [] # keeps all objects reachable during the hold phase
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
_log_vm_stats("before allocation")
|
|
119
|
+
|
|
120
|
+
total_bytes = max(1, int(leaksINMB * MEGA))
|
|
121
|
+
# Default 30 KB if objectSizeINKB <= 0
|
|
122
|
+
object_size_kb = objectSizeINKB if objectSizeINKB > 0 else 30.0
|
|
123
|
+
object_size_bytes = max(1024, int(object_size_kb * 1024))
|
|
124
|
+
|
|
125
|
+
object_count = max(1, math.ceil(total_bytes / object_size_bytes))
|
|
126
|
+
|
|
127
|
+
delay_per_object = (shootUpDurationInSec / object_count) if shootUpDurationInSec > 0 else 0.0
|
|
128
|
+
|
|
129
|
+
logger.info(
|
|
130
|
+
"MemoryLeakHavoc: Heap pressure plan | targetBytes=%d | objectSizeBytes=%d | "
|
|
131
|
+
"objectCount=%d | delayPerObject=%.4fs",
|
|
132
|
+
total_bytes, object_size_bytes, object_count, delay_per_object
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
allocated_bytes = 0
|
|
136
|
+
for i in range(object_count):
|
|
137
|
+
if _stop_event.is_set():
|
|
138
|
+
logger.info("MemoryLeakHavoc: stop signal received during allocation, stopping after %d/%d objects", i, object_count)
|
|
139
|
+
break
|
|
140
|
+
try:
|
|
141
|
+
obj = MemoryLeakObject(object_size_bytes)
|
|
142
|
+
memory_leaks.append(obj) # retain reference — keeps object reachable
|
|
143
|
+
allocated_bytes += object_size_bytes
|
|
144
|
+
except MemoryError:
|
|
145
|
+
logger.error(
|
|
146
|
+
"MemoryError at object %d/%d after %d bytes — stopping ramp",
|
|
147
|
+
i + 1, object_count, allocated_bytes
|
|
148
|
+
)
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
if delay_per_object > 0:
|
|
152
|
+
if _stop_event.wait(timeout=delay_per_object):
|
|
153
|
+
logger.info("MemoryLeakHavoc: stop signal received during ramp-up delay")
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
_log_vm_stats("after allocation (%.1fMB held)" % (allocated_bytes / MEGA))
|
|
157
|
+
logger.info("MemoryLeakHavoc: Heap pressure allocation complete — %d objects holding ~%d bytes",
|
|
158
|
+
len(memory_leaks), allocated_bytes)
|
|
159
|
+
|
|
160
|
+
hold_secs = max(0.0, totalDurationInSec - shootUpDurationInSec)
|
|
161
|
+
logger.info("MemoryLeakHavoc: Holding heap pressure for %.1fs", hold_secs)
|
|
162
|
+
if hold_secs > 0:
|
|
163
|
+
if _stop_event.wait(timeout=hold_secs):
|
|
164
|
+
logger.info("MemoryLeakHavoc: stop signal received during hold phase")
|
|
165
|
+
|
|
166
|
+
except Exception:
|
|
167
|
+
logger.exception("MemoryLeakHavoc: Unexpected error in heap pressure havoc")
|
|
168
|
+
finally:
|
|
169
|
+
# Drop all references → objects become GC-eligible
|
|
170
|
+
for obj in memory_leaks:
|
|
171
|
+
obj.clear()
|
|
172
|
+
memory_leaks.clear()
|
|
173
|
+
gc.collect() # nudge GC to reclaim PyFloatObjects immediately after hold
|
|
174
|
+
_log_vm_stats("after release")
|
|
175
|
+
logger.warning("MemoryLeakHavoc: Heap Pressure Havoc complete")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _log_vm_stats(label: str = "") -> None:
|
|
179
|
+
try:
|
|
180
|
+
tot, avail, percent, used, free = get_virtual_memory()
|
|
181
|
+
logger.warning(
|
|
182
|
+
"MemoryLeakHavoc: VM [%s]: total=%.2fGB used=%.2fGB avail=%.2fGB free=%.2fGB percent=%.1f%%",
|
|
183
|
+
label, tot / GIGA, used / GIGA, avail / GIGA, free / GIGA, percent
|
|
184
|
+
)
|
|
185
|
+
except Exception:
|
|
186
|
+
logger.exception("MemoryLeakHavoc: Could not read VM stats")
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger('pythonagent.agent')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ThreadAllocationError(RuntimeError):
|
|
9
|
+
"""Raised when a havoc worker thread cannot be allocated."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Module-level so a stop request (from havoc_manager.parse_nethavoc_config) can
|
|
13
|
+
# reach a thread-leak havoc that's currently running in its own daemon thread.
|
|
14
|
+
_stop_event = threading.Event()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def stop_thread_leak():
|
|
18
|
+
"""Signal any running thread-leak havoc to stop immediately."""
|
|
19
|
+
logger.info("ThreadLeakHavoc: stop_thread_leak called")
|
|
20
|
+
_stop_event.set()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def monitored_sleep(duration_sec):
|
|
24
|
+
"""Sleep for duration_sec while monitoring actual elapsed time.
|
|
25
|
+
|
|
26
|
+
This helper detects early wake-up from the sleep call and logs the
|
|
27
|
+
requested duration vs actual elapsed time.
|
|
28
|
+
"""
|
|
29
|
+
start = time.monotonic()
|
|
30
|
+
try:
|
|
31
|
+
time.sleep(duration_sec)
|
|
32
|
+
finally:
|
|
33
|
+
end = time.monotonic()
|
|
34
|
+
elapsed = end - start
|
|
35
|
+
logger.debug("ThreadLeakHavoc: Requested sleep: %.3fs, elapsed: %.3fs", duration_sec, elapsed)
|
|
36
|
+
if elapsed < duration_sec:
|
|
37
|
+
logger.warning("ThreadLeakHavoc: Sleep returned early: requested=%.3fs elapsed=%.3fs", duration_sec, elapsed)
|
|
38
|
+
else:
|
|
39
|
+
logger.debug("ThreadLeakHavoc: Sleep completed or overslept: requested=%.3fs elapsed=%.3fs", duration_sec, elapsed)
|
|
40
|
+
return elapsed
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def apply_thread_leak(numberOfThreads, threadSleepTimeInMS, shootUpDuration, stableDuration, tumbleDuration):
|
|
44
|
+
"""Apply thread-leak havoc with ramp-up, stable, and ramp-down phases."""
|
|
45
|
+
|
|
46
|
+
def thread_sleep_function(stop_event):
|
|
47
|
+
# Worker thread waits until the stop event is set.
|
|
48
|
+
while not stop_event.is_set() and not _stop_event.is_set():
|
|
49
|
+
stop_event.wait(thread_sleep_time_sec)
|
|
50
|
+
|
|
51
|
+
def start_havoc_thread():
|
|
52
|
+
# Create and start a new daemon thread that will sleep repeatedly.
|
|
53
|
+
stop_event = threading.Event()
|
|
54
|
+
thread_name = "TLHavoc{}".format(len(active_threads))
|
|
55
|
+
thread = threading.Thread(target=thread_sleep_function, args=(stop_event,),
|
|
56
|
+
name=thread_name, daemon=True)
|
|
57
|
+
try:
|
|
58
|
+
thread.start()
|
|
59
|
+
except RuntimeError as exc:
|
|
60
|
+
stop_event.set()
|
|
61
|
+
logger.exception("ThreadLeakHavoc: failed to start a worker thread: %s", exc)
|
|
62
|
+
raise ThreadAllocationError("new thread could not be allocated") from exc
|
|
63
|
+
stop_events.append(stop_event)
|
|
64
|
+
active_threads.append(thread)
|
|
65
|
+
|
|
66
|
+
def stop_havoc_threads(count):
|
|
67
|
+
# Signal `count` threads to stop and remove them from tracking lists.
|
|
68
|
+
for _ in range(min(count, len(stop_events))):
|
|
69
|
+
stop_event = stop_events.pop(0)
|
|
70
|
+
thread = active_threads.pop(0)
|
|
71
|
+
stop_event.set()
|
|
72
|
+
if thread.is_alive():
|
|
73
|
+
thread.join(timeout=1.0)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Track stop events and running threads for this thread-leak instance.
|
|
77
|
+
stop_events = []
|
|
78
|
+
active_threads = []
|
|
79
|
+
|
|
80
|
+
# Reset any stop signal left over from a previous run so this run starts clean.
|
|
81
|
+
_stop_event.clear()
|
|
82
|
+
|
|
83
|
+
# Begin the thread-leak havoc setup with the requested parameters.
|
|
84
|
+
logger.info("ThreadLeakHavoc: starting thread leak setup requested=%d sleep_ms=%s", numberOfThreads, threadSleepTimeInMS)
|
|
85
|
+
|
|
86
|
+
# Normalize inputs and convert sleep duration to seconds.
|
|
87
|
+
thread_sleep_time_sec = max(threadSleepTimeInMS, 1) / 1000.0
|
|
88
|
+
shootUpDuration = max(shootUpDuration, 0)
|
|
89
|
+
tumbleDuration = max(tumbleDuration, 0)
|
|
90
|
+
stableDuration = max(stableDuration, 0)
|
|
91
|
+
|
|
92
|
+
logger.info("ThreadLeakHavoc: apply_thread_leak called: requested=%d, sleep_ms=%s, shootUp=%d, stable=%d, tumble=%d", numberOfThreads, threadSleepTimeInMS, shootUpDuration, stableDuration, tumbleDuration)
|
|
93
|
+
|
|
94
|
+
if numberOfThreads == 0:
|
|
95
|
+
logger.info("ThreadLeakHavoc: Thread leak requested with zero threads")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Ramp-up phase: gradually create the requested number of worker threads.
|
|
99
|
+
startTime = time.monotonic()
|
|
100
|
+
for i in range(shootUpDuration):
|
|
101
|
+
if _stop_event.is_set():
|
|
102
|
+
logger.info("ThreadLeakHavoc: stop signal received during ramp-up")
|
|
103
|
+
stop_havoc_threads(len(active_threads))
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
threads_per_second = max(1, int(round((numberOfThreads - len(active_threads)) / max(1, shootUpDuration - i))))
|
|
107
|
+
logger.debug("ThreadLeakHavoc: ramp-up target per second=%d", threads_per_second)
|
|
108
|
+
stime = time.monotonic()
|
|
109
|
+
for _ in range(threads_per_second):
|
|
110
|
+
try:
|
|
111
|
+
start_havoc_thread()
|
|
112
|
+
except ThreadAllocationError:
|
|
113
|
+
logger.error("ThreadLeakHavoc: thread creation failed before full allocation; successfully created=%d", len(active_threads))
|
|
114
|
+
er= "Havoc failed because new thread could not be allocated after " + str(len(active_threads))
|
|
115
|
+
stop_havoc_threads(len(active_threads))
|
|
116
|
+
raise ThreadAllocationError(er)
|
|
117
|
+
|
|
118
|
+
if len(active_threads) >= numberOfThreads:
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
logger.debug("ThreadLeakHavoc: Thread in this Second=%d, in time=%.3f", threads_per_second, time.monotonic() - stime)
|
|
122
|
+
elapsed = time.monotonic() - stime
|
|
123
|
+
if elapsed < 1.0:
|
|
124
|
+
time.sleep(1.0 - elapsed)
|
|
125
|
+
else:
|
|
126
|
+
logger.warning("ThreadLeakHavoc: ramp-up loop exceeded its 1s budget; continuing immediately")
|
|
127
|
+
|
|
128
|
+
while len(active_threads) < numberOfThreads:
|
|
129
|
+
if _stop_event.is_set():
|
|
130
|
+
logger.info("ThreadLeakHavoc: stop signal received before full allocation")
|
|
131
|
+
stop_havoc_threads(len(active_threads))
|
|
132
|
+
return
|
|
133
|
+
try:
|
|
134
|
+
start_havoc_thread()
|
|
135
|
+
except ThreadAllocationError:
|
|
136
|
+
logger.error("ThreadLeakHavoc: thread creation failed before full allocation; successfully created=%d", len(active_threads))
|
|
137
|
+
er= "Havoc failed because new thread could not be allocated after " + str(len(active_threads))
|
|
138
|
+
stop_havoc_threads(len(active_threads))
|
|
139
|
+
raise ThreadAllocationError(er)
|
|
140
|
+
|
|
141
|
+
logger.info("ThreadLeakHavoc: Thread leak allocation completed active_threads: %d, in time: %.3f", len(active_threads), time.monotonic() - startTime)
|
|
142
|
+
|
|
143
|
+
# Stable phase: hold thread count steady for the configured duration,
|
|
144
|
+
# checking every second so a stop request cuts it short instead of
|
|
145
|
+
# blocking for the full stableDuration.
|
|
146
|
+
elapsed = 0
|
|
147
|
+
while elapsed < stableDuration:
|
|
148
|
+
if _stop_event.is_set():
|
|
149
|
+
logger.info("ThreadLeakHavoc: stop signal received during stable phase")
|
|
150
|
+
stop_havoc_threads(len(active_threads))
|
|
151
|
+
return
|
|
152
|
+
monitored_sleep(1)
|
|
153
|
+
elapsed += 1
|
|
154
|
+
|
|
155
|
+
# Ramp-down phase: gradually stop threads over the tumble duration.
|
|
156
|
+
logger.info("ThreadLeakHavoc: entering ramp-down phase")
|
|
157
|
+
startTime = time.monotonic()
|
|
158
|
+
for i in range(tumbleDuration):
|
|
159
|
+
if _stop_event.is_set():
|
|
160
|
+
logger.info("ThreadLeakHavoc: stop signal received during ramp-down")
|
|
161
|
+
break
|
|
162
|
+
logger.info("ThreadLeakHavoc: Starting ramp-down: tumbleDuration=%d, active_threads=%d", tumbleDuration, len(active_threads))
|
|
163
|
+
threads_per_second = max(1, int(round(len(active_threads) / max(1, tumbleDuration - i))))
|
|
164
|
+
stime = time.monotonic()
|
|
165
|
+
stop_havoc_threads(threads_per_second)
|
|
166
|
+
elapsed = time.monotonic() - stime
|
|
167
|
+
if elapsed < 1.0:
|
|
168
|
+
time.sleep(1.0 - elapsed)
|
|
169
|
+
else:
|
|
170
|
+
logger.warning("ThreadLeakHavoc: ramp-down loop exceeded its 1s budget; continuing immediately")
|
|
171
|
+
|
|
172
|
+
if len(active_threads):
|
|
173
|
+
logger.info("ThreadLeakHavoc: Stopping active threads without rampdown")
|
|
174
|
+
stop_havoc_threads(len(active_threads))
|
|
175
|
+
|
|
176
|
+
logger.info("ThreadLeakHavoc: Thread leak completed: requested=%d, remaining_active=%d, time_taken=%f", numberOfThreads, len(active_threads), time.monotonic() - startTime)
|
|
177
|
+
except ThreadAllocationError:
|
|
178
|
+
er= "Havoc failed because new thread could not be allocated after " + str(len(active_threads))
|
|
179
|
+
logger.exception("ThreadLeakHavoc: Thread allocation failed after creating=%d threads, propagating to manager", len(active_threads))
|
|
180
|
+
stop_havoc_threads(len(active_threads))
|
|
181
|
+
raise ThreadAllocationError(er)
|
|
182
|
+
except Exception:
|
|
183
|
+
logger.exception("ThreadLeakHavoc: Thread allocation failed after creating=%d threads, propagating to manager", len(active_threads))
|
|
184
|
+
stop_havoc_threads(len(active_threads))
|
|
185
|
+
finally:
|
|
186
|
+
if len(active_threads):
|
|
187
|
+
stop_havoc_threads(len(active_threads))
|