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.
Files changed (180) hide show
  1. cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
  2. cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
  3. cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
  4. cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
  5. cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
  6. pythonagent/__init__.py +22 -0
  7. pythonagent/agent/__init__.py +219 -0
  8. pythonagent/agent/internal/__init__.py +1 -0
  9. pythonagent/agent/internal/agent.py +1651 -0
  10. pythonagent/agent/internal/framesinfo.py +152 -0
  11. pythonagent/agent/internal/heap_dump.py +39 -0
  12. pythonagent/agent/internal/intercept_module.py +70 -0
  13. pythonagent/agent/internal/logs.py +122 -0
  14. pythonagent/agent/internal/metadata/__init__.py +0 -0
  15. pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
  16. pythonagent/agent/internal/proc_compat.py +75 -0
  17. pythonagent/agent/internal/profile.py +47 -0
  18. pythonagent/agent/internal/provider.py +83 -0
  19. pythonagent/agent/internal/thread_dump.py +85 -0
  20. pythonagent/agent/internal/udp.py +245 -0
  21. pythonagent/agent/internal/udp_message.py +800 -0
  22. pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
  23. pythonagent/agent/probes/Instrumentation/find.py +243 -0
  24. pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
  25. pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
  26. pythonagent/agent/probes/Instrumentation/parser.py +129 -0
  27. pythonagent/agent/probes/__init__.py +219 -0
  28. pythonagent/agent/probes/base.py +303 -0
  29. pythonagent/agent/probes/cache/__init__.py +51 -0
  30. pythonagent/agent/probes/cache/redis.py +119 -0
  31. pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
  32. pythonagent/agent/probes/coroutines/__init__.py +1 -0
  33. pythonagent/agent/probes/coroutines/asyncio.py +63 -0
  34. pythonagent/agent/probes/elasticdb/__init__.py +7 -0
  35. pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
  36. pythonagent/agent/probes/frameworks/__init__.py +27 -0
  37. pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
  38. pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
  39. pythonagent/agent/probes/frameworks/aisess.py +151 -0
  40. pythonagent/agent/probes/frameworks/asgi.py +340 -0
  41. pythonagent/agent/probes/frameworks/bottle.py +27 -0
  42. pythonagent/agent/probes/frameworks/cherry.py +25 -0
  43. pythonagent/agent/probes/frameworks/django.py +128 -0
  44. pythonagent/agent/probes/frameworks/falcon.py +21 -0
  45. pythonagent/agent/probes/frameworks/fastapi.py +35 -0
  46. pythonagent/agent/probes/frameworks/flask.py +30 -0
  47. pythonagent/agent/probes/frameworks/pyramid.py +56 -0
  48. pythonagent/agent/probes/frameworks/test.py +108 -0
  49. pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
  50. pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
  51. pythonagent/agent/probes/frameworks/wsgi.py +353 -0
  52. pythonagent/agent/probes/grpc/__init__.py +76 -0
  53. pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
  54. pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
  55. pythonagent/agent/probes/havoc/__init__.py +0 -0
  56. pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
  57. pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
  58. pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
  59. pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
  60. pythonagent/agent/probes/http/__init__.py +49 -0
  61. pythonagent/agent/probes/http/aiohttp_client.py +59 -0
  62. pythonagent/agent/probes/http/boto.py +12 -0
  63. pythonagent/agent/probes/http/httplib.py +110 -0
  64. pythonagent/agent/probes/http/httpx_client.py +116 -0
  65. pythonagent/agent/probes/http/requests.py +15 -0
  66. pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
  67. pythonagent/agent/probes/http/urllib3.py +16 -0
  68. pythonagent/agent/probes/langchain/__init__.py +21 -0
  69. pythonagent/agent/probes/langchain/base_tool.py +95 -0
  70. pythonagent/agent/probes/langchain/langchain_community.py +136 -0
  71. pythonagent/agent/probes/langchain/langchain_core.py +32 -0
  72. pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
  73. pythonagent/agent/probes/logging/__init__.py +106 -0
  74. pythonagent/agent/probes/message_brokers/__init__.py +4 -0
  75. pythonagent/agent/probes/message_brokers/pika.py +126 -0
  76. pythonagent/agent/probes/mongodb/__init__.py +6 -0
  77. pythonagent/agent/probes/mongodb/pymongo.py +286 -0
  78. pythonagent/agent/probes/openai/__init__.py +3 -0
  79. pythonagent/agent/probes/openai/openai.py +797 -0
  80. pythonagent/agent/probes/span.py +101 -0
  81. pythonagent/agent/probes/sql/__init__.py +13 -0
  82. pythonagent/agent/probes/sql/botocores3.py +51 -0
  83. pythonagent/agent/probes/sql/dbapi.py +285 -0
  84. pythonagent/agent/probes/sql/dynamodb.py +90 -0
  85. pythonagent/agent/probes/sql/mysql_connector.py +24 -0
  86. pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
  87. pythonagent/agent/probes/sql/mysqldb.py +43 -0
  88. pythonagent/agent/probes/sql/psycopg2.py +174 -0
  89. pythonagent/agent/probes/sql/pymysql.py +25 -0
  90. pythonagent/bootstrap/__init__.py +0 -0
  91. pythonagent/bootstrap/cav_gunicorn.py +26 -0
  92. pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
  93. pythonagent/bootstrap/run.py +47 -0
  94. pythonagent/bootstrap/sitecustomize.py +287 -0
  95. pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
  96. pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
  97. pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
  98. pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
  99. pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
  100. pythonagent/config.py +279 -0
  101. pythonagent/find.py +72 -0
  102. pythonagent/find_mod_cls_name.py +54 -0
  103. pythonagent/lang.py +131 -0
  104. pythonagent/lib.py +91 -0
  105. pythonagent/main/__init__.py +0 -0
  106. pythonagent/main/pytrace/__init__.py +79 -0
  107. pythonagent/main/pytrace/commands/__init__.py +0 -0
  108. pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
  109. pythonagent/main/pytrace/commands/run.py +401 -0
  110. pythonagent/main/pytrace/pytrace.py +133 -0
  111. pythonagent/main/wsgi.py +6 -0
  112. pythonagent/main.py +27 -0
  113. pythonagent/run.py +46 -0
  114. pythonagent/sqins.py +8 -0
  115. pythonagent/test.py +73 -0
  116. pythonagent/utils.py +168 -0
  117. pythonagent/vendor/__init__.py +0 -0
  118. pythonagent/vendor/pympler/__init__.py +1 -0
  119. pythonagent/vendor/pympler/asizeof.py +2810 -0
  120. pythonagent/vendor/pympler/charts.py +62 -0
  121. pythonagent/vendor/pympler/classtracker.py +590 -0
  122. pythonagent/vendor/pympler/classtracker_stats.py +780 -0
  123. pythonagent/vendor/pympler/garbagegraph.py +80 -0
  124. pythonagent/vendor/pympler/mprofile.py +97 -0
  125. pythonagent/vendor/pympler/muppy.py +275 -0
  126. pythonagent/vendor/pympler/panels.py +115 -0
  127. pythonagent/vendor/pympler/process.py +238 -0
  128. pythonagent/vendor/pympler/py.typed +0 -0
  129. pythonagent/vendor/pympler/refbrowser.py +451 -0
  130. pythonagent/vendor/pympler/refgraph.py +350 -0
  131. pythonagent/vendor/pympler/summary.py +321 -0
  132. pythonagent/vendor/pympler/tracker.py +267 -0
  133. pythonagent/vendor/pympler/util/__init__.py +0 -0
  134. pythonagent/vendor/pympler/util/bottle.py +3809 -0
  135. pythonagent/vendor/pympler/util/compat.py +23 -0
  136. pythonagent/vendor/pympler/util/stringutils.py +77 -0
  137. pythonagent/vendor/pympler/web.py +346 -0
  138. pythonagent/vendor/werkzeug/__init__.py +20 -0
  139. pythonagent/vendor/werkzeug/_compat.py +228 -0
  140. pythonagent/vendor/werkzeug/_internal.py +473 -0
  141. pythonagent/vendor/werkzeug/_reloader.py +341 -0
  142. pythonagent/vendor/werkzeug/datastructures.py +3120 -0
  143. pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
  144. pythonagent/vendor/werkzeug/debug/console.py +218 -0
  145. pythonagent/vendor/werkzeug/debug/repr.py +297 -0
  146. pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
  147. pythonagent/vendor/werkzeug/exceptions.py +829 -0
  148. pythonagent/vendor/werkzeug/filesystem.py +64 -0
  149. pythonagent/vendor/werkzeug/formparser.py +584 -0
  150. pythonagent/vendor/werkzeug/http.py +1307 -0
  151. pythonagent/vendor/werkzeug/local.py +420 -0
  152. pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
  153. pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
  154. pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
  155. pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
  156. pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
  157. pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
  158. pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
  159. pythonagent/vendor/werkzeug/posixemulation.py +117 -0
  160. pythonagent/vendor/werkzeug/routing.py +2210 -0
  161. pythonagent/vendor/werkzeug/security.py +249 -0
  162. pythonagent/vendor/werkzeug/serving.py +1117 -0
  163. pythonagent/vendor/werkzeug/test.py +1123 -0
  164. pythonagent/vendor/werkzeug/testapp.py +241 -0
  165. pythonagent/vendor/werkzeug/urls.py +1138 -0
  166. pythonagent/vendor/werkzeug/useragents.py +202 -0
  167. pythonagent/vendor/werkzeug/utils.py +778 -0
  168. pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
  169. pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
  170. pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
  171. pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
  172. pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
  173. pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
  174. pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
  175. pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
  176. pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
  177. pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
  178. pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
  179. pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
  180. 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))