flowcept 0.8.10__py3-none-any.whl → 0.8.11__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.
- flowcept/cli.py +210 -10
- flowcept/commons/daos/keyvalue_dao.py +19 -23
- flowcept/commons/daos/mq_dao/mq_dao_base.py +29 -29
- flowcept/commons/daos/mq_dao/mq_dao_kafka.py +4 -3
- flowcept/commons/daos/mq_dao/mq_dao_mofka.py +4 -0
- flowcept/commons/daos/mq_dao/mq_dao_redis.py +38 -5
- flowcept/commons/daos/redis_conn.py +47 -0
- flowcept/commons/flowcept_dataclasses/task_object.py +36 -8
- flowcept/commons/settings_factory.py +2 -4
- flowcept/commons/task_data_preprocess.py +200 -0
- flowcept/commons/utils.py +1 -1
- flowcept/configs.py +8 -4
- flowcept/flowcept_api/flowcept_controller.py +30 -13
- flowcept/flowceptor/adapters/agents/__init__.py +1 -0
- flowcept/flowceptor/adapters/agents/agents_utils.py +89 -0
- flowcept/flowceptor/adapters/agents/flowcept_agent.py +292 -0
- flowcept/flowceptor/adapters/agents/flowcept_llm_prov_capture.py +186 -0
- flowcept/flowceptor/adapters/agents/prompts.py +51 -0
- flowcept/flowceptor/adapters/base_interceptor.py +13 -6
- flowcept/flowceptor/adapters/brokers/__init__.py +1 -0
- flowcept/flowceptor/adapters/brokers/mqtt_interceptor.py +132 -0
- flowcept/flowceptor/adapters/mlflow/mlflow_interceptor.py +3 -3
- flowcept/flowceptor/adapters/tensorboard/tensorboard_interceptor.py +3 -3
- flowcept/flowceptor/consumers/agent/__init__.py +1 -0
- flowcept/flowceptor/consumers/agent/base_agent_context_manager.py +101 -0
- flowcept/flowceptor/consumers/agent/client_agent.py +48 -0
- flowcept/flowceptor/consumers/agent/flowcept_agent_context_manager.py +145 -0
- flowcept/flowceptor/consumers/agent/flowcept_qa_manager.py +112 -0
- flowcept/flowceptor/consumers/base_consumer.py +90 -0
- flowcept/flowceptor/consumers/document_inserter.py +135 -36
- flowcept/flowceptor/telemetry_capture.py +1 -1
- flowcept/instrumentation/task_capture.py +8 -2
- flowcept/version.py +1 -1
- {flowcept-0.8.10.dist-info → flowcept-0.8.11.dist-info}/METADATA +10 -1
- {flowcept-0.8.10.dist-info → flowcept-0.8.11.dist-info}/RECORD +39 -27
- resources/sample_settings.yaml +37 -13
- flowcept/flowceptor/adapters/zambeze/__init__.py +0 -1
- flowcept/flowceptor/adapters/zambeze/zambeze_dataclasses.py +0 -41
- flowcept/flowceptor/adapters/zambeze/zambeze_interceptor.py +0 -102
- {flowcept-0.8.10.dist-info → flowcept-0.8.11.dist-info}/WHEEL +0 -0
- {flowcept-0.8.10.dist-info → flowcept-0.8.11.dist-info}/entry_points.txt +0 -0
- {flowcept-0.8.10.dist-info → flowcept-0.8.11.dist-info}/licenses/LICENSE +0 -0
flowcept/cli.py
CHANGED
|
@@ -14,6 +14,9 @@ Supports:
|
|
|
14
14
|
- `flowcept --help --command` for command-specific help
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
import subprocess
|
|
18
|
+
from time import sleep
|
|
19
|
+
from typing import Dict, Optional
|
|
17
20
|
import argparse
|
|
18
21
|
import os
|
|
19
22
|
import sys
|
|
@@ -21,6 +24,8 @@ import json
|
|
|
21
24
|
import textwrap
|
|
22
25
|
import inspect
|
|
23
26
|
from functools import wraps
|
|
27
|
+
from importlib import resources
|
|
28
|
+
from pathlib import Path
|
|
24
29
|
from typing import List
|
|
25
30
|
|
|
26
31
|
from flowcept import Flowcept, configs
|
|
@@ -46,11 +51,31 @@ def show_config():
|
|
|
46
51
|
}
|
|
47
52
|
print(f"This is the settings path in this session: {configs.SETTINGS_PATH}")
|
|
48
53
|
print(
|
|
49
|
-
f"This is your FLOWCEPT_SETTINGS_PATH environment variable value: "
|
|
50
|
-
f"{config_data['env_FLOWCEPT_SETTINGS_PATH']}"
|
|
54
|
+
f"This is your FLOWCEPT_SETTINGS_PATH environment variable value: {config_data['env_FLOWCEPT_SETTINGS_PATH']}"
|
|
51
55
|
)
|
|
52
56
|
|
|
53
57
|
|
|
58
|
+
def init_settings():
|
|
59
|
+
"""
|
|
60
|
+
Create a new settings.yaml file in your home directory under ~/.flowcept.
|
|
61
|
+
"""
|
|
62
|
+
dest_path = Path(os.path.join(configs._SETTINGS_DIR, "settings.yaml"))
|
|
63
|
+
|
|
64
|
+
if dest_path.exists():
|
|
65
|
+
overwrite = input(f"{dest_path} already exists. Overwrite? (y/N): ").strip().lower()
|
|
66
|
+
if overwrite != "y":
|
|
67
|
+
print("Operation aborted.")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
os.makedirs(configs._SETTINGS_DIR, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
SAMPLE_SETTINGS_PATH = str(resources.files("resources").joinpath("sample_settings.yaml"))
|
|
73
|
+
|
|
74
|
+
with open(SAMPLE_SETTINGS_PATH, "rb") as src_file, open(dest_path, "wb") as dst_file:
|
|
75
|
+
dst_file.write(src_file.read())
|
|
76
|
+
print(f"Copied {configs.SETTINGS_PATH} to {dest_path}")
|
|
77
|
+
|
|
78
|
+
|
|
54
79
|
def start_consumption_services(bundle_exec_id: str = None, check_safe_stops: bool = False, consumers: List[str] = None):
|
|
55
80
|
"""
|
|
56
81
|
Start services that consume data from a queue or other source.
|
|
@@ -121,28 +146,202 @@ def workflow_count(workflow_id: str):
|
|
|
121
146
|
print(json.dumps(result, indent=2))
|
|
122
147
|
|
|
123
148
|
|
|
124
|
-
def query(
|
|
149
|
+
def query(filter: str, project: str = None, sort: str = None, limit: int = 0):
|
|
150
|
+
"""
|
|
151
|
+
Query the MongoDB task collection with an optional projection, sort, and limit.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
filter : str
|
|
156
|
+
A JSON string representing the MongoDB filter query.
|
|
157
|
+
project : str, optional
|
|
158
|
+
A JSON string specifying fields to include or exclude in the result (MongoDB projection).
|
|
159
|
+
sort : str, optional
|
|
160
|
+
A JSON string specifying sorting criteria (e.g., '[["started_at", -1]]').
|
|
161
|
+
limit : int, optional
|
|
162
|
+
Maximum number of documents to return. Default is 0 (no limit).
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
List[dict]
|
|
167
|
+
A list of task documents matching the query.
|
|
168
|
+
"""
|
|
169
|
+
_filter = json.loads(filter)
|
|
170
|
+
_project = json.loads(project) or None
|
|
171
|
+
_sort = list(sort) or None
|
|
172
|
+
print(
|
|
173
|
+
json.dumps(Flowcept.db.query(filter=_filter, project=_project, sort=_sort, limit=limit), indent=2, default=str)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_task(task_id: str):
|
|
125
178
|
"""
|
|
126
|
-
Query the Document DB.
|
|
179
|
+
Query the Document DB to retrieve a task.
|
|
127
180
|
|
|
128
181
|
Parameters
|
|
129
182
|
----------
|
|
130
|
-
|
|
131
|
-
|
|
183
|
+
task_id : str
|
|
184
|
+
The identifier of the task.
|
|
185
|
+
"""
|
|
186
|
+
_query = {"task_id": task_id}
|
|
187
|
+
print(json.dumps(Flowcept.db.query(_query), indent=2, default=str))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def start_agent():
|
|
191
|
+
"""Start Flowcept agent."""
|
|
192
|
+
from flowcept.flowceptor.adapters.agents.flowcept_agent import main
|
|
193
|
+
|
|
194
|
+
main()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def agent_client(tool_name: str, kwargs: str = None):
|
|
198
|
+
"""Agent Client.
|
|
199
|
+
|
|
200
|
+
Parameters.
|
|
201
|
+
----------
|
|
202
|
+
tool_name : str
|
|
203
|
+
Name of the tool
|
|
204
|
+
kwargs : str, optional
|
|
205
|
+
A stringfied JSON containing the kwargs for the tool, if needed.
|
|
206
|
+
"""
|
|
207
|
+
print(kwargs)
|
|
208
|
+
if kwargs is not None:
|
|
209
|
+
kwargs = json.loads(kwargs)
|
|
210
|
+
|
|
211
|
+
print(f"Going to run agent tool '{tool_name}'.")
|
|
212
|
+
if kwargs:
|
|
213
|
+
print(f"Using kwargs: {kwargs}")
|
|
214
|
+
print("-----------------")
|
|
215
|
+
from flowcept.flowceptor.consumers.agent.client_agent import run_tool
|
|
216
|
+
|
|
217
|
+
result = run_tool(tool_name, kwargs)[0]
|
|
218
|
+
|
|
219
|
+
print(result.text)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def check_services():
|
|
223
|
+
"""
|
|
224
|
+
Run a full diagnostic test on the Flowcept system and its dependencies.
|
|
225
|
+
|
|
226
|
+
This function:
|
|
227
|
+
- Prints the current configuration path.
|
|
228
|
+
- Checks if required services (e.g., MongoDB, agent) are alive.
|
|
229
|
+
- Runs a test function wrapped with Flowcept instrumentation.
|
|
230
|
+
- Verifies MongoDB insertion (if enabled).
|
|
231
|
+
- Verifies agent communication and LLM connectivity (if enabled).
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
None
|
|
236
|
+
Prints diagnostics to stdout; returns nothing.
|
|
132
237
|
"""
|
|
133
|
-
|
|
134
|
-
|
|
238
|
+
print(f"Testing with settings at: {configs.SETTINGS_PATH}")
|
|
239
|
+
from flowcept.configs import MONGO_ENABLED, AGENT, KVDB_ENABLED, INSERTION_BUFFER_TIME
|
|
240
|
+
|
|
241
|
+
if not Flowcept.services_alive():
|
|
242
|
+
print("Some of the enabled services are not alive!")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
check_safe_stops = KVDB_ENABLED
|
|
246
|
+
|
|
247
|
+
from uuid import uuid4
|
|
248
|
+
from flowcept.instrumentation.flowcept_task import flowcept_task
|
|
249
|
+
|
|
250
|
+
workflow_id = str(uuid4())
|
|
251
|
+
|
|
252
|
+
@flowcept_task
|
|
253
|
+
def test_function(n: int) -> Dict[str, int]:
|
|
254
|
+
return {"output": n + 1}
|
|
255
|
+
|
|
256
|
+
with Flowcept(workflow_id=workflow_id, check_safe_stops=check_safe_stops):
|
|
257
|
+
test_function(2)
|
|
258
|
+
|
|
259
|
+
if MONGO_ENABLED:
|
|
260
|
+
print("MongoDB is enabled, so we are testing it too.")
|
|
261
|
+
tasks = Flowcept.db.query({"workflow_id": workflow_id})
|
|
262
|
+
if len(tasks) != 1:
|
|
263
|
+
print(f"The query result, {len(tasks)}, is not what we expected.")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if AGENT.get("enabled", False):
|
|
267
|
+
print("Agent is enabled, so we are testing it too.")
|
|
268
|
+
from flowcept.flowceptor.consumers.agent.client_agent import run_tool
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
print(run_tool("check_liveness"))
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print(e)
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
print("Testing LLM connectivity")
|
|
277
|
+
check_llm_result = run_tool("check_llm")[0]
|
|
278
|
+
print(check_llm_result.text)
|
|
279
|
+
|
|
280
|
+
if "error" in check_llm_result.text.lower():
|
|
281
|
+
print("There is an error with the LLM communication.")
|
|
282
|
+
return
|
|
283
|
+
elif MONGO_ENABLED:
|
|
284
|
+
print("Testing if llm chat was stored in MongoDB.")
|
|
285
|
+
response_metadata = json.loads(check_llm_result.text.split("\n")[0])
|
|
286
|
+
print(response_metadata)
|
|
287
|
+
sleep(INSERTION_BUFFER_TIME * 1.05)
|
|
288
|
+
chats = Flowcept.db.query({"workflow_id": response_metadata["agent_id"]})
|
|
289
|
+
if chats:
|
|
290
|
+
print(chats)
|
|
291
|
+
else:
|
|
292
|
+
print("Could not find chat history. Make sure that the DB Inserter service is on.")
|
|
293
|
+
print("\n\nAll expected services seem to be working properly!")
|
|
294
|
+
return
|
|
135
295
|
|
|
136
296
|
|
|
137
297
|
COMMAND_GROUPS = [
|
|
138
|
-
("Basic Commands", [show_config, start_services, stop_services]),
|
|
298
|
+
("Basic Commands", [check_services, show_config, init_settings, start_services, stop_services]),
|
|
139
299
|
("Consumption Commands", [start_consumption_services, stop_consumption_services]),
|
|
140
|
-
("Database Commands", [workflow_count, query]),
|
|
300
|
+
("Database Commands", [workflow_count, query, get_task]),
|
|
301
|
+
("Agent Commands", [start_agent, agent_client]),
|
|
141
302
|
]
|
|
142
303
|
|
|
143
304
|
COMMANDS = set(f for _, fs in COMMAND_GROUPS for f in fs)
|
|
144
305
|
|
|
145
306
|
|
|
307
|
+
def _run_command(cmd_str: str, check_output: bool = True, popen_kwargs: Optional[Dict] = None) -> Optional[str]:
|
|
308
|
+
"""
|
|
309
|
+
Run a shell command with optional output capture.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
cmd_str : str
|
|
314
|
+
The command to execute.
|
|
315
|
+
check_output : bool, optional
|
|
316
|
+
If True, capture and return the command's standard output.
|
|
317
|
+
If False, run interactively (stdout/stderr goes to terminal).
|
|
318
|
+
popen_kwargs : dict, optional
|
|
319
|
+
Extra keyword arguments to pass to subprocess.run.
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
output : str or None
|
|
324
|
+
The standard output of the command if check_output is True, else None.
|
|
325
|
+
|
|
326
|
+
Raises
|
|
327
|
+
------
|
|
328
|
+
subprocess.CalledProcessError
|
|
329
|
+
If the command exits with a non-zero status.
|
|
330
|
+
"""
|
|
331
|
+
if popen_kwargs is None:
|
|
332
|
+
popen_kwargs = {}
|
|
333
|
+
|
|
334
|
+
kwargs = {"shell": True, "check": True, **popen_kwargs}
|
|
335
|
+
|
|
336
|
+
if check_output:
|
|
337
|
+
kwargs.update({"capture_output": True, "text": True})
|
|
338
|
+
result = subprocess.run(cmd_str, **kwargs)
|
|
339
|
+
return result.stdout.strip()
|
|
340
|
+
else:
|
|
341
|
+
subprocess.run(cmd_str, **kwargs)
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
146
345
|
def _parse_numpy_doc(docstring: str):
|
|
147
346
|
parsed = {}
|
|
148
347
|
lines = docstring.splitlines() if docstring else []
|
|
@@ -258,3 +457,4 @@ def main(): # noqa: D103
|
|
|
258
457
|
|
|
259
458
|
if __name__ == "__main__":
|
|
260
459
|
main()
|
|
460
|
+
# check_services()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Key value module."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from flowcept.commons.daos.redis_conn import RedisConn
|
|
4
4
|
|
|
5
5
|
from flowcept.commons.flowcept_logger import FlowceptLogger
|
|
6
6
|
from flowcept.configs import (
|
|
@@ -24,32 +24,13 @@ class KeyValueDAO:
|
|
|
24
24
|
cls._instance = super(KeyValueDAO, cls).__new__(cls)
|
|
25
25
|
return cls._instance
|
|
26
26
|
|
|
27
|
-
@staticmethod
|
|
28
|
-
def build_redis_conn_pool():
|
|
29
|
-
"""Utility function to build Redis connection."""
|
|
30
|
-
pool = ConnectionPool(
|
|
31
|
-
host=KVDB_HOST,
|
|
32
|
-
port=KVDB_PORT,
|
|
33
|
-
db=0,
|
|
34
|
-
password=KVDB_PASSWORD,
|
|
35
|
-
decode_responses=False,
|
|
36
|
-
max_connections=10000, # TODO: Config file
|
|
37
|
-
socket_keepalive=True,
|
|
38
|
-
retry_on_timeout=True,
|
|
39
|
-
)
|
|
40
|
-
return Redis(connection_pool=pool)
|
|
41
|
-
# return Redis()
|
|
42
|
-
|
|
43
27
|
def __init__(self):
|
|
44
28
|
if not hasattr(self, "_initialized"):
|
|
45
29
|
self._initialized = True
|
|
46
30
|
self.logger = FlowceptLogger()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else:
|
|
51
|
-
# Otherwise, use the host, port, and password settings
|
|
52
|
-
self.redis_conn = KeyValueDAO.build_redis_conn_pool()
|
|
31
|
+
self.redis_conn = RedisConn.build_redis_conn_pool(
|
|
32
|
+
host=KVDB_HOST, port=KVDB_PORT, password=KVDB_PASSWORD, uri=KVDB_URI
|
|
33
|
+
)
|
|
53
34
|
|
|
54
35
|
def delete_set(self, set_name: str):
|
|
55
36
|
"""Delete it."""
|
|
@@ -133,3 +114,18 @@ class KeyValueDAO:
|
|
|
133
114
|
None
|
|
134
115
|
"""
|
|
135
116
|
self.redis_conn.delete(key)
|
|
117
|
+
|
|
118
|
+
def liveness_test(self):
|
|
119
|
+
"""Get the livelyness of it."""
|
|
120
|
+
try:
|
|
121
|
+
response = self.redis_conn.ping()
|
|
122
|
+
if response:
|
|
123
|
+
return True
|
|
124
|
+
else:
|
|
125
|
+
return False
|
|
126
|
+
except ConnectionError as e:
|
|
127
|
+
self.logger.exception(e)
|
|
128
|
+
return False
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.logger.exception(e)
|
|
131
|
+
return False
|
|
@@ -20,6 +20,7 @@ from flowcept.configs import (
|
|
|
20
20
|
MQ_CHUNK_SIZE,
|
|
21
21
|
MQ_TYPE,
|
|
22
22
|
MQ_TIMING,
|
|
23
|
+
KVDB_ENABLED,
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
from flowcept.commons.utils import GenericJSONEncoder
|
|
@@ -67,7 +68,13 @@ class MQDao(ABC):
|
|
|
67
68
|
self.logger = FlowceptLogger()
|
|
68
69
|
self.started = False
|
|
69
70
|
self._adapter_settings = adapter_settings
|
|
70
|
-
|
|
71
|
+
if KVDB_ENABLED:
|
|
72
|
+
self._keyvalue_dao = KeyValueDAO()
|
|
73
|
+
else:
|
|
74
|
+
self._keyvalue_dao = None
|
|
75
|
+
self.logger.warning(
|
|
76
|
+
"We are going to run without KVDB. If you are running a workflow, this may lead to errors."
|
|
77
|
+
)
|
|
71
78
|
self._time_based_flushing_started = False
|
|
72
79
|
self.buffer: Union[AutoflushBuffer, List] = None
|
|
73
80
|
if MQ_TIMING:
|
|
@@ -138,7 +145,7 @@ class MQDao(ABC):
|
|
|
138
145
|
"""
|
|
139
146
|
self._keyvalue_dao.delete_key("current_campaign_id")
|
|
140
147
|
|
|
141
|
-
def init_buffer(self, interceptor_instance_id: str, exec_bundle_id=None):
|
|
148
|
+
def init_buffer(self, interceptor_instance_id: str, exec_bundle_id=None, check_safe_stops=True):
|
|
142
149
|
"""Create the buffer."""
|
|
143
150
|
if not self.started:
|
|
144
151
|
if flowcept.configs.DB_FLUSH_MODE == "online":
|
|
@@ -147,7 +154,8 @@ class MQDao(ABC):
|
|
|
147
154
|
max_size=MQ_BUFFER_SIZE,
|
|
148
155
|
flush_interval=MQ_INSERTION_BUFFER_TIME,
|
|
149
156
|
)
|
|
150
|
-
|
|
157
|
+
if check_safe_stops:
|
|
158
|
+
self.register_time_based_thread_init(interceptor_instance_id, exec_bundle_id)
|
|
151
159
|
self._time_based_flushing_started = True
|
|
152
160
|
else:
|
|
153
161
|
self.buffer = list()
|
|
@@ -164,9 +172,9 @@ class MQDao(ABC):
|
|
|
164
172
|
self.bulk_publish(self.buffer)
|
|
165
173
|
self.buffer = list()
|
|
166
174
|
|
|
167
|
-
def _stop_timed(self, interceptor_instance_id: str, bundle_exec_id: int = None):
|
|
175
|
+
def _stop_timed(self, interceptor_instance_id: str, check_safe_stops: bool = True, bundle_exec_id: int = None):
|
|
168
176
|
t1 = time()
|
|
169
|
-
self._stop(interceptor_instance_id, bundle_exec_id)
|
|
177
|
+
self._stop(interceptor_instance_id, check_safe_stops, bundle_exec_id)
|
|
170
178
|
t2 = time()
|
|
171
179
|
self._flush_events.append(["final", t1, t2, t2 - t1, "n/a"])
|
|
172
180
|
|
|
@@ -175,14 +183,14 @@ class MQDao(ABC):
|
|
|
175
183
|
writer.writerow(["type", "start", "end", "duration", "size"])
|
|
176
184
|
writer.writerows(self._flush_events)
|
|
177
185
|
|
|
178
|
-
def _stop(self, interceptor_instance_id: str, bundle_exec_id: int = None):
|
|
179
|
-
"""Stop
|
|
180
|
-
|
|
181
|
-
self.logger.debug(msg0 + f"{bundle_exec_id}; interceptor id: {interceptor_instance_id}")
|
|
186
|
+
def _stop(self, interceptor_instance_id: str, check_safe_stops: bool = True, bundle_exec_id: int = None):
|
|
187
|
+
"""Stop MQ publisher."""
|
|
188
|
+
self.logger.debug(f"MQ pub received stop sign: bundle={bundle_exec_id}, interceptor={interceptor_instance_id}")
|
|
182
189
|
self._close_buffer()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
self.logger.debug("Flushed MQ for the last time!")
|
|
191
|
+
if check_safe_stops:
|
|
192
|
+
self.logger.debug(f"Sending stop msg. Bundle: {bundle_exec_id}; interceptor id: {interceptor_instance_id}")
|
|
193
|
+
self._send_mq_dao_time_thread_stop(interceptor_instance_id, bundle_exec_id)
|
|
186
194
|
self.started = False
|
|
187
195
|
|
|
188
196
|
def _send_mq_dao_time_thread_stop(self, interceptor_instance_id, exec_bundle_id=None):
|
|
@@ -197,10 +205,10 @@ class MQDao(ABC):
|
|
|
197
205
|
# self.logger.info("Control msg sent: " + str(msg))
|
|
198
206
|
self.send_message(msg)
|
|
199
207
|
|
|
200
|
-
def send_document_inserter_stop(self):
|
|
208
|
+
def send_document_inserter_stop(self, exec_bundle_id=None):
|
|
201
209
|
"""Send the document."""
|
|
202
210
|
# These control_messages are handled by the document inserter
|
|
203
|
-
msg = {"type": "flowcept_control", "info": "stop_document_inserter"}
|
|
211
|
+
msg = {"type": "flowcept_control", "info": "stop_document_inserter", "exec_bundle_id": exec_bundle_id}
|
|
204
212
|
self.send_message(msg)
|
|
205
213
|
|
|
206
214
|
@abstractmethod
|
|
@@ -223,20 +231,12 @@ class MQDao(ABC):
|
|
|
223
231
|
"""Subscribe to the interception channel."""
|
|
224
232
|
raise NotImplementedError()
|
|
225
233
|
|
|
234
|
+
@abstractmethod
|
|
235
|
+
def unsubscribe(self):
|
|
236
|
+
"""Subscribe to the interception channel."""
|
|
237
|
+
raise NotImplementedError()
|
|
238
|
+
|
|
226
239
|
@abstractmethod
|
|
227
240
|
def liveness_test(self) -> bool:
|
|
228
|
-
"""
|
|
229
|
-
|
|
230
|
-
"""
|
|
231
|
-
try:
|
|
232
|
-
response = self._keyvalue_dao.redis_conn.ping()
|
|
233
|
-
if response:
|
|
234
|
-
return True
|
|
235
|
-
else:
|
|
236
|
-
return False
|
|
237
|
-
except ConnectionError as e:
|
|
238
|
-
self.logger.exception(e)
|
|
239
|
-
return False
|
|
240
|
-
except Exception as e:
|
|
241
|
-
self.logger.exception(e)
|
|
242
|
-
return False
|
|
241
|
+
"""Checks if the MQ system is alive."""
|
|
242
|
+
raise NotImplementedError()
|
|
@@ -108,12 +108,13 @@ class MQDaoKafka(MQDao):
|
|
|
108
108
|
def liveness_test(self):
|
|
109
109
|
"""Get the livelyness of it."""
|
|
110
110
|
try:
|
|
111
|
-
if not super().liveness_test():
|
|
112
|
-
self.logger.error("KV Store not alive!")
|
|
113
|
-
return False
|
|
114
111
|
admin_client = AdminClient(self._kafka_conf)
|
|
115
112
|
kafka_metadata = admin_client.list_topics(timeout=5)
|
|
116
113
|
return MQ_CHANNEL in kafka_metadata.topics
|
|
117
114
|
except Exception as e:
|
|
118
115
|
self.logger.exception(e)
|
|
119
116
|
return False
|
|
117
|
+
|
|
118
|
+
def unsubscribe(self):
|
|
119
|
+
"""Unsubscribes from Kafka topic."""
|
|
120
|
+
raise NotImplementedError()
|
|
@@ -7,7 +7,8 @@ import msgpack
|
|
|
7
7
|
from time import time, sleep
|
|
8
8
|
|
|
9
9
|
from flowcept.commons.daos.mq_dao.mq_dao_base import MQDao
|
|
10
|
-
from flowcept.
|
|
10
|
+
from flowcept.commons.daos.redis_conn import RedisConn
|
|
11
|
+
from flowcept.configs import MQ_CHANNEL, MQ_HOST, MQ_PORT, MQ_PASSWORD, MQ_URI, MQ_SETTINGS, KVDB_ENABLED
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class MQDaoRedis(MQDao):
|
|
@@ -17,16 +18,32 @@ class MQDaoRedis(MQDao):
|
|
|
17
18
|
|
|
18
19
|
def __init__(self, adapter_settings=None):
|
|
19
20
|
super().__init__(adapter_settings)
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
self._consumer = None
|
|
23
|
+
use_same_as_kv = MQ_SETTINGS.get("same_as_kvdb", False)
|
|
24
|
+
if use_same_as_kv:
|
|
25
|
+
if KVDB_ENABLED:
|
|
26
|
+
self._producer = self._keyvalue_dao.redis_conn
|
|
27
|
+
else:
|
|
28
|
+
raise Exception("You have same_as_kvdb in your settings, but kvdb is disabled.")
|
|
29
|
+
else:
|
|
30
|
+
self._producer = RedisConn.build_redis_conn_pool(
|
|
31
|
+
host=MQ_HOST, port=MQ_PORT, password=MQ_PASSWORD, uri=MQ_URI
|
|
32
|
+
)
|
|
22
33
|
|
|
23
34
|
def subscribe(self):
|
|
24
35
|
"""
|
|
25
36
|
Subscribe to interception channel.
|
|
26
37
|
"""
|
|
27
|
-
self._consumer = self.
|
|
38
|
+
self._consumer = self._producer.pubsub()
|
|
28
39
|
self._consumer.psubscribe(MQ_CHANNEL)
|
|
29
40
|
|
|
41
|
+
def unsubscribe(self):
|
|
42
|
+
"""
|
|
43
|
+
Unsubscribe to interception channel.
|
|
44
|
+
"""
|
|
45
|
+
self._consumer.unsubscribe(MQ_CHANNEL)
|
|
46
|
+
|
|
30
47
|
def message_listener(self, message_handler: Callable):
|
|
31
48
|
"""Get message listener with automatic reconnection."""
|
|
32
49
|
max_retrials = 10
|
|
@@ -37,13 +54,22 @@ class MQDaoRedis(MQDao):
|
|
|
37
54
|
for message in self._consumer.listen():
|
|
38
55
|
if message and message["type"] in MQDaoRedis.MESSAGE_TYPES_IGNORE:
|
|
39
56
|
continue
|
|
57
|
+
|
|
58
|
+
if not isinstance(message["data"], (bytes, bytearray)):
|
|
59
|
+
self.logger.warning(
|
|
60
|
+
f"Skipping message with unexpected data type: {type(message['data'])} - {message['data']}"
|
|
61
|
+
)
|
|
62
|
+
continue
|
|
63
|
+
|
|
40
64
|
try:
|
|
41
65
|
msg_obj = msgpack.loads(message["data"], strict_map_key=False)
|
|
66
|
+
# self.logger.debug(f"In mq dao redis, received msg! {msg_obj}")
|
|
42
67
|
if not message_handler(msg_obj):
|
|
43
68
|
should_continue = False # Break While loop
|
|
44
69
|
break # Break For loop
|
|
45
70
|
except Exception as e:
|
|
46
|
-
self.logger.error(f"Failed to process message
|
|
71
|
+
self.logger.error(f"Failed to process message {message}")
|
|
72
|
+
self.logger.exception(e)
|
|
47
73
|
|
|
48
74
|
current_trials = 0
|
|
49
75
|
except (redis.exceptions.ConnectionError, redis.exceptions.TimeoutError) as e:
|
|
@@ -103,7 +129,14 @@ class MQDaoRedis(MQDao):
|
|
|
103
129
|
def liveness_test(self):
|
|
104
130
|
"""Get the livelyness of it."""
|
|
105
131
|
try:
|
|
106
|
-
|
|
132
|
+
response = self._producer.ping()
|
|
133
|
+
if response:
|
|
134
|
+
return True
|
|
135
|
+
else:
|
|
136
|
+
return False
|
|
137
|
+
except ConnectionError as e:
|
|
138
|
+
self.logger.exception(e)
|
|
139
|
+
return False
|
|
107
140
|
except Exception as e:
|
|
108
141
|
self.logger.exception(e)
|
|
109
142
|
return False
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""RedisConn module."""
|
|
2
|
+
|
|
3
|
+
from redis import Redis, ConnectionPool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RedisConn:
|
|
7
|
+
"""RedisConn DAO class."""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def build_redis_conn_pool(host: str = None, port: str = None, password: str = None, uri: str = None) -> Redis:
|
|
11
|
+
"""
|
|
12
|
+
Create a Redis connection using either a URI or host/port.
|
|
13
|
+
|
|
14
|
+
If `uri` is provided, it will be used to initialize the Redis connection.
|
|
15
|
+
Otherwise, the connection will fall back to using `host` and `port`.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
host : str, optional
|
|
20
|
+
Redis host address. Used only if `uri` is not provided.
|
|
21
|
+
port : str, optional
|
|
22
|
+
Redis port. Used only if `uri` is not provided.
|
|
23
|
+
uri : str, optional
|
|
24
|
+
Full Redis URI. Takes precedence over `host` and `port` if defined.
|
|
25
|
+
password : str, optional
|
|
26
|
+
Password for authenticating with Redis.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
Redis
|
|
31
|
+
An instance of the Redis client with a configured connection pool.
|
|
32
|
+
"""
|
|
33
|
+
pool_kwargs = {
|
|
34
|
+
"db": 0,
|
|
35
|
+
"password": password,
|
|
36
|
+
"decode_responses": False,
|
|
37
|
+
"max_connections": 10000,
|
|
38
|
+
"socket_keepalive": True,
|
|
39
|
+
"retry_on_timeout": True,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if uri:
|
|
43
|
+
pool = ConnectionPool.from_url(uri, **pool_kwargs)
|
|
44
|
+
else:
|
|
45
|
+
pool = ConnectionPool(host=host, port=port, **pool_kwargs)
|
|
46
|
+
|
|
47
|
+
return Redis(connection_pool=pool)
|
|
@@ -30,7 +30,7 @@ class TaskObject:
|
|
|
30
30
|
submitted_at: float = None
|
|
31
31
|
started_at: float = None
|
|
32
32
|
ended_at: float = None
|
|
33
|
-
registered_at: float = None
|
|
33
|
+
registered_at: float = None # Leave this for dates generated at the DocInserter
|
|
34
34
|
telemetry_at_start: Telemetry = None
|
|
35
35
|
telemetry_at_end: Telemetry = None
|
|
36
36
|
workflow_name: AnyStr = None
|
|
@@ -52,6 +52,7 @@ class TaskObject:
|
|
|
52
52
|
address: AnyStr = None
|
|
53
53
|
dependencies: List = None
|
|
54
54
|
dependents: List = None
|
|
55
|
+
tags: List = None
|
|
55
56
|
|
|
56
57
|
_DEFAULT_ENRICH_VALUES = {
|
|
57
58
|
"node_name": NODE_NAME,
|
|
@@ -145,10 +146,37 @@ class TaskObject:
|
|
|
145
146
|
if (key not in task_dict or task_dict[key] is None) and fallback_value is not None:
|
|
146
147
|
task_dict[key] = fallback_value
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
@staticmethod
|
|
150
|
+
def from_dict(task_obj_dict: Dict[AnyStr, Any]) -> "TaskObject":
|
|
151
|
+
"""Create a TaskObject from a dictionary.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
task_obj_dict : Dict[AnyStr, Any]
|
|
156
|
+
Dictionary containing task attributes.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
TaskObject
|
|
161
|
+
A TaskObject instance populated with available data.
|
|
162
|
+
"""
|
|
163
|
+
task = TaskObject()
|
|
164
|
+
|
|
165
|
+
for key, value in task_obj_dict.items():
|
|
166
|
+
if hasattr(task, key):
|
|
167
|
+
if key == "status" and isinstance(value, str):
|
|
168
|
+
setattr(task, key, Status(value))
|
|
169
|
+
else:
|
|
170
|
+
setattr(task, key, value)
|
|
171
|
+
|
|
172
|
+
return task
|
|
173
|
+
|
|
174
|
+
def __str__(self):
|
|
175
|
+
"""Return a user-friendly string representation of the TaskObject."""
|
|
176
|
+
return self.__repr__()
|
|
177
|
+
|
|
178
|
+
def __repr__(self):
|
|
179
|
+
"""Return an unambiguous string representation of the TaskObject."""
|
|
180
|
+
attrs = ["task_id", "workflow_id", "campaign_id", "activity_id", "custom_metadata", "started_at", "ended_at"]
|
|
181
|
+
attr_str = ", ".join(f"{attr}={repr(getattr(self, attr))}" for attr in attrs)
|
|
182
|
+
return f"TaskObject({attr_str})"
|