firexapp 5.7.162__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.
- firex_blaze/__init__.py +1 -0
- firex_blaze/__main__.py +109 -0
- firex_blaze/blaze_event_consumer.py +284 -0
- firex_blaze/blaze_helper.py +67 -0
- firex_blaze/blaze_launcher.py +144 -0
- firex_blaze/fast_blaze_helper.py +11 -0
- firex_bundle_ci/__init__.py +5 -0
- firex_bundle_ci/tasks.py +216 -0
- firex_flame/__init__.py +1 -0
- firex_flame/__main__.py +209 -0
- firex_flame/api.py +447 -0
- firex_flame/controller.py +437 -0
- firex_flame/event_broker_processor.py +201 -0
- firex_flame/event_file_processor.py +85 -0
- firex_flame/flame_helper.py +213 -0
- firex_flame/flame_task_graph.py +1206 -0
- firex_flame/launcher.py +202 -0
- firex_flame/main_app.py +66 -0
- firex_flame/model_dumper.py +195 -0
- firex_flame/templates/index.html +18 -0
- firex_flame/web_app.py +194 -0
- firex_flame_ui/COMMITHASH +1 -0
- firex_flame_ui/VERSION +1 -0
- firex_flame_ui/__init__.py +5 -0
- firex_flame_ui/assets/firex_logo.6409b05e.png +0 -0
- firex_flame_ui/assets/index.3a452d74.css +7 -0
- firex_flame_ui/assets/index.e869f273.js +637 -0
- firex_flame_ui/index.html +15 -0
- firex_flame_ui/send-firex-user-config.html +13 -0
- firex_keeper/__init__.py +1 -0
- firex_keeper/__main__.py +67 -0
- firex_keeper/db_model.py +61 -0
- firex_keeper/keeper_event_consumer.py +379 -0
- firex_keeper/keeper_helper.py +49 -0
- firex_keeper/keeper_launcher.py +57 -0
- firex_keeper/persist.py +258 -0
- firex_keeper/task_query.py +343 -0
- firexapp/__init__.py +6 -0
- firexapp/__main__.py +2 -0
- firexapp/application.py +230 -0
- firexapp/broker_manager/__init__.py +31 -0
- firexapp/broker_manager/broker_factory.py +83 -0
- firexapp/broker_manager/redis_manager.py +484 -0
- firexapp/celery_manager.py +373 -0
- firexapp/common.py +244 -0
- firexapp/discovery.py +167 -0
- firexapp/engine/__init__.py +0 -0
- firexapp/engine/celery.py +73 -0
- firexapp/engine/default_celery_config.py +169 -0
- firexapp/engine/firex_revoke.py +158 -0
- firexapp/engine/logging.py +212 -0
- firexapp/engine/run_controller.py +93 -0
- firexapp/events/__init__.py +0 -0
- firexapp/events/broker_event_consumer.py +144 -0
- firexapp/events/event_aggregator.py +372 -0
- firexapp/events/model.py +245 -0
- firexapp/fileregistry.py +64 -0
- firexapp/firex_subprocess.py +590 -0
- firexapp/info.py +289 -0
- firexapp/plugins.py +369 -0
- firexapp/reporters/__init__.py +0 -0
- firexapp/reporters/json_reporter.py +656 -0
- firexapp/submit/__init__.py +0 -0
- firexapp/submit/arguments.py +272 -0
- firexapp/submit/console.py +149 -0
- firexapp/submit/install_configs.py +166 -0
- firexapp/submit/report_trigger.py +27 -0
- firexapp/submit/reporting.py +220 -0
- firexapp/submit/shutdown.py +249 -0
- firexapp/submit/submit.py +774 -0
- firexapp/submit/tracking_service.py +50 -0
- firexapp/submit/uid.py +185 -0
- firexapp/tasks/__init__.py +0 -0
- firexapp/tasks/core_tasks.py +99 -0
- firexapp/tasks/example.py +159 -0
- firexapp/tasks/root_tasks.py +87 -0
- firexapp/testing/__init__.py +0 -0
- firexapp/testing/config_base.py +120 -0
- firexapp/testing/config_interpreter.py +195 -0
- firexapp/testing/coverage_plugin.py +41 -0
- firexapp/testing/pydev_debug_plugin.py +112 -0
- firexapp/testing/test_infra.py +167 -0
- firexapp-5.7.162.dist-info/METADATA +77 -0
- firexapp-5.7.162.dist-info/RECORD +107 -0
- firexapp-5.7.162.dist-info/WHEEL +4 -0
- firexapp-5.7.162.dist-info/entry_points.txt +20 -0
- firexapp-5.7.162.dist-info/licenses/LICENSE +29 -0
- firexkit/__init__.py +1 -0
- firexkit/argument_conversion.py +304 -0
- firexkit/bag_of_goodies.py +784 -0
- firexkit/broker.py +72 -0
- firexkit/chain.py +356 -0
- firexkit/firex_exceptions.py +38 -0
- firexkit/firexkit_common.py +34 -0
- firexkit/inspect.py +101 -0
- firexkit/install_resources/cloud-ci-install-configs.json +10 -0
- firexkit/memory_utils.py +59 -0
- firexkit/permissions.py +3 -0
- firexkit/proc_utils.py +44 -0
- firexkit/resources/firex.css +176 -0
- firexkit/resources/firex_logo.png +0 -0
- firexkit/resources.py +24 -0
- firexkit/result.py +1006 -0
- firexkit/revoke.py +95 -0
- firexkit/task.py +1832 -0
- firexkit/templates/link.html +8 -0
- firexkit/templates/log_template.html +90 -0
firex_blaze/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from firexapp import __version__
|
firex_blaze/__main__.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import logging.handlers
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import signal
|
|
7
|
+
|
|
8
|
+
# Prevent dependencies from taking module loading hit of pkg_resources.
|
|
9
|
+
sys.modules["pkg_resources"] = type('noop', (object,), {})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from firexapp.events.model import FireXRunMetadata
|
|
13
|
+
|
|
14
|
+
from firex_blaze.blaze_event_consumer import BlazeKafkaSenderThread
|
|
15
|
+
from firex_blaze.fast_blaze_helper import get_blaze_dir
|
|
16
|
+
from firex_blaze.blaze_helper import BlazeSenderConfig, get_blaze_events_file, celery_app_from_logs_dir
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_blaze_args():
|
|
22
|
+
parser = argparse.ArgumentParser()
|
|
23
|
+
parser.add_argument("--instance_name", help="Name of the blaze instance")
|
|
24
|
+
parser.add_argument("--logs_dir", help="Logs directory for the run to keep task data for.",
|
|
25
|
+
required=True)
|
|
26
|
+
parser.add_argument("--uid", help="FireX UID for the run to keep task data for.",
|
|
27
|
+
required=True)
|
|
28
|
+
parser.add_argument("--firex_requester", help="Requester username, if different than running user",
|
|
29
|
+
required=True)
|
|
30
|
+
parser.add_argument('--broker_recv_ready_file', help='File to create immediately before capturing celery events.',
|
|
31
|
+
default=None)
|
|
32
|
+
parser.add_argument('--logs_url', help='Webserver used from which logs can be accessed.',
|
|
33
|
+
default=None)
|
|
34
|
+
|
|
35
|
+
parser.add_argument('--kafka_topic', help='Topic use for the Kafka bus', required=True)
|
|
36
|
+
parser.add_argument('--bootstrap_servers', help='Comma seperated list of Kafka bootrap servers.', required=True)
|
|
37
|
+
parser.add_argument('--security_protocol', help='Protocol used to communicate with brokers. '
|
|
38
|
+
'Valid values are: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL. '
|
|
39
|
+
'Default: PLAINTEXT.',
|
|
40
|
+
default='PLAINTEXT')
|
|
41
|
+
|
|
42
|
+
# SASL-SSL OAuth 2.0 arguments
|
|
43
|
+
parser.add_argument('--sasl_mechanism', help='SASL mechanism (e.g., OAUTHBEARER).',
|
|
44
|
+
default=None)
|
|
45
|
+
parser.add_argument('--sasl_oauthbearer_method', help='OAuth bearer method (e.g., oidc).',
|
|
46
|
+
default=None)
|
|
47
|
+
parser.add_argument('--sasl_oauthbearer_client_id', help='OAuth client ID.',
|
|
48
|
+
default=None)
|
|
49
|
+
parser.add_argument('--sasl_oauthbearer_client_secret', help='OAuth client secret.',
|
|
50
|
+
default=None)
|
|
51
|
+
parser.add_argument('--sasl_oauthbearer_token_endpoint_url', help='OAuth token endpoint URL.',
|
|
52
|
+
default=None)
|
|
53
|
+
parser.add_argument('--ssl_ca_location', help='CA certificate location for SSL verification.',
|
|
54
|
+
default=None)
|
|
55
|
+
|
|
56
|
+
return parser.parse_args()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def init_blaze():
|
|
60
|
+
args = _parse_blaze_args()
|
|
61
|
+
|
|
62
|
+
run_metadata = FireXRunMetadata(args.uid, args.logs_dir, None, None, args.firex_requester)
|
|
63
|
+
|
|
64
|
+
blaze_dir = get_blaze_dir(run_metadata.logs_dir, args.instance_name)
|
|
65
|
+
os.makedirs(blaze_dir, exist_ok=True)
|
|
66
|
+
logging.basicConfig(
|
|
67
|
+
level=logging.DEBUG,
|
|
68
|
+
handlers=[
|
|
69
|
+
logging.handlers.RotatingFileHandler(
|
|
70
|
+
os.path.join(blaze_dir, 'blaze.log'),
|
|
71
|
+
maxBytes=20*1024*1024, # Max size of a log file (20 MB)
|
|
72
|
+
backupCount=1,
|
|
73
|
+
)
|
|
74
|
+
],
|
|
75
|
+
format='[%(asctime)s][%(levelname)s][%(name)s]: %(message)s',
|
|
76
|
+
datefmt="%Y-%m-%d %H:%M:%S")
|
|
77
|
+
|
|
78
|
+
logging.getLogger('kafka.producer').setLevel(logging.INFO)
|
|
79
|
+
logger.info(f'Starting Blaze with args: {args}')
|
|
80
|
+
|
|
81
|
+
signal.signal(signal.SIGTERM, lambda _, __: sys.exit(1))
|
|
82
|
+
|
|
83
|
+
celery_app = celery_app_from_logs_dir(run_metadata.logs_dir)
|
|
84
|
+
blaze_sender_config = BlazeSenderConfig(
|
|
85
|
+
kafka_topic=args.kafka_topic,
|
|
86
|
+
kafka_bootstrap_servers=args.bootstrap_servers.split(','),
|
|
87
|
+
max_kafka_connection_retries=2,
|
|
88
|
+
security_protocol=args.security_protocol,
|
|
89
|
+
sasl_mechanism=args.sasl_mechanism,
|
|
90
|
+
sasl_oauthbearer_method=args.sasl_oauthbearer_method,
|
|
91
|
+
sasl_oauthbearer_client_id=args.sasl_oauthbearer_client_id,
|
|
92
|
+
sasl_oauthbearer_client_secret=args.sasl_oauthbearer_client_secret,
|
|
93
|
+
sasl_oauthbearer_token_endpoint_url=args.sasl_oauthbearer_token_endpoint_url,
|
|
94
|
+
ssl_ca_location=args.ssl_ca_location,
|
|
95
|
+
)
|
|
96
|
+
recording_file = get_blaze_events_file(run_metadata.logs_dir, args.instance_name)
|
|
97
|
+
return celery_app, run_metadata, args.broker_recv_ready_file, blaze_sender_config, args.logs_url, recording_file
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main():
|
|
101
|
+
celery_app, run_metadata, receiver_ready_file, blaze_sender_config, logs_url, recording_file = init_blaze()
|
|
102
|
+
BlazeKafkaSenderThread(
|
|
103
|
+
celery_app, run_metadata, blaze_sender_config, logs_url,
|
|
104
|
+
receiver_ready_file=receiver_ready_file,
|
|
105
|
+
recording_file=recording_file).run()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == '__main__':
|
|
109
|
+
main()
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Process events from Celery and put them on a kafka bus.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from getpass import getuser
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
|
|
11
|
+
from confluent_kafka import Producer, KafkaException
|
|
12
|
+
from confluent_kafka.admin import AdminClient
|
|
13
|
+
|
|
14
|
+
from firexapp.events.broker_event_consumer import BrokerEventConsumerThread
|
|
15
|
+
from firexapp.events.model import FireXRunMetadata, COMPLETE_RUNSTATES, RunStates
|
|
16
|
+
|
|
17
|
+
from firex_blaze.blaze_helper import BlazeSenderConfig, KAFKA_EVENTS_FILE_DELIMITER
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
TASK_EVENT_TO_STATE = {
|
|
22
|
+
'task-started-info': 'STARTED',
|
|
23
|
+
RunStates.FAILED.to_celery_event_type(): 'FAILURE',
|
|
24
|
+
RunStates.SUCCEEDED.to_celery_event_type(): 'SUCCESS',
|
|
25
|
+
RunStates.REVOKED.to_celery_event_type(): 'REVOKED',
|
|
26
|
+
RunStates.REVOKE_COMPLETED.to_celery_event_type(): 'REVOKED',
|
|
27
|
+
# historically were mapped but never sent.
|
|
28
|
+
# 'task-sent': 'PENDING',
|
|
29
|
+
# 'task-received': 'RECEIVED',
|
|
30
|
+
# 'task-started': 'STARTED',
|
|
31
|
+
# 'task-rejected': 'REJECTED',
|
|
32
|
+
# 'task-retried': 'RETRY',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BLAZE_SEND_EVENT_TYPES = tuple(
|
|
36
|
+
list(TASK_EVENT_TO_STATE.keys()) + ['task-completed', 'task-results', 'task-instrumentation', 'task-args']
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_kafka_message(firex_id, event_data, uuid, logs_url, submitter=getuser(), firex_requester=None) -> dict[str, Any]:
|
|
41
|
+
return {'FIREX_ID': firex_id,
|
|
42
|
+
'SUBMITTER': submitter,
|
|
43
|
+
'FIREX_REQUESTER': firex_requester,
|
|
44
|
+
'LOGS_URL': logs_url, # Shouldn't be required, but Lumens needs it!
|
|
45
|
+
'EVENTS': [{'DATA': event_data,
|
|
46
|
+
'UUID': uuid}]}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def send_kafka_mssg(kafka_producer: Producer, kafka_mssg: dict[str, Any], kafka_topic: str, firex_id: str,
|
|
51
|
+
partition: Optional[int] = None):
|
|
52
|
+
"""Send message using confluent-kafka Producer."""
|
|
53
|
+
try:
|
|
54
|
+
kafka_producer.produce(
|
|
55
|
+
topic=kafka_topic,
|
|
56
|
+
value=json.dumps(kafka_mssg).encode('ascii'),
|
|
57
|
+
key=firex_id.encode('ascii'),
|
|
58
|
+
)
|
|
59
|
+
# Poll to handle any internal events and queued delivery reports (in case of delivery callback functions)
|
|
60
|
+
kafka_producer.poll(0)
|
|
61
|
+
except BufferError:
|
|
62
|
+
# Buffer is full, wait for messages to be delivered
|
|
63
|
+
logger.warning('Kafka producer buffer full, waiting for delivery...')
|
|
64
|
+
kafka_producer.poll(1)
|
|
65
|
+
# Retry the send
|
|
66
|
+
kafka_producer.produce(
|
|
67
|
+
topic=kafka_topic,
|
|
68
|
+
value=json.dumps(kafka_mssg).encode('ascii'),
|
|
69
|
+
key=firex_id.encode('ascii'),
|
|
70
|
+
)
|
|
71
|
+
kafka_producer.poll(0)
|
|
72
|
+
except KafkaException as e:
|
|
73
|
+
logger.error(f'Failed to send Kafka message: {e}')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_basic_event(name, event_type, timestamp=None, event_timestamp=time.time()):
|
|
77
|
+
if timestamp is None:
|
|
78
|
+
timestamp = event_timestamp
|
|
79
|
+
|
|
80
|
+
event_data = {'name': name,
|
|
81
|
+
'type': event_type,
|
|
82
|
+
'timestamp': timestamp, # Shouldn't be required, but Lumens needs it!
|
|
83
|
+
'event_timestamp': event_timestamp}
|
|
84
|
+
|
|
85
|
+
# Not all types map to states (e.g. task-results), so only populate state for some event types.
|
|
86
|
+
if event_type in TASK_EVENT_TO_STATE:
|
|
87
|
+
event_data['state'] = TASK_EVENT_TO_STATE[event_type]
|
|
88
|
+
|
|
89
|
+
return event_data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class NoNameForEvent(Exception):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class KafkaSenderThread(BrokerEventConsumerThread):
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
celery_app,
|
|
101
|
+
run_metadata: FireXRunMetadata,
|
|
102
|
+
config: BlazeSenderConfig,
|
|
103
|
+
max_retry_attempts: Optional[int] = None,
|
|
104
|
+
receiver_ready_file: Optional[str] = None,
|
|
105
|
+
recording_file: Optional[str] = None,
|
|
106
|
+
partition: Optional[int] = None,
|
|
107
|
+
):
|
|
108
|
+
|
|
109
|
+
super().__init__(celery_app, max_retry_attempts, receiver_ready_file)
|
|
110
|
+
self.firex_id = run_metadata.firex_id
|
|
111
|
+
self.kafka_topic = config.kafka_topic
|
|
112
|
+
self.recording_file = recording_file
|
|
113
|
+
self.partition = partition
|
|
114
|
+
|
|
115
|
+
# Connect to bootstrap servers and get a Producer instance
|
|
116
|
+
self.producer = self.get_kafka_producer(config)
|
|
117
|
+
logger.debug (f'Kafka producer created for topic {self.kafka_topic}')
|
|
118
|
+
self.root_task = {'uuid': None, 'is_complete': False}
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def get_kafka_producer(cls, config: BlazeSenderConfig) -> Producer:
|
|
122
|
+
"""Create confluent-kafka Producer with retry logic."""
|
|
123
|
+
_retries = 0
|
|
124
|
+
|
|
125
|
+
# Build configuration dictionary for confluent-kafka
|
|
126
|
+
producer_config = {
|
|
127
|
+
'bootstrap.servers': ','.join(config.kafka_bootstrap_servers)
|
|
128
|
+
if isinstance(config.kafka_bootstrap_servers, list)
|
|
129
|
+
else config.kafka_bootstrap_servers,
|
|
130
|
+
'security.protocol': config.security_protocol,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Add SASL-SSL OAuth parameters
|
|
134
|
+
if config.sasl_mechanism:
|
|
135
|
+
producer_config['sasl.mechanism'] = config.sasl_mechanism
|
|
136
|
+
if config.sasl_oauthbearer_method:
|
|
137
|
+
producer_config['sasl.oauthbearer.method'] = config.sasl_oauthbearer_method
|
|
138
|
+
if config.sasl_oauthbearer_client_id:
|
|
139
|
+
producer_config['sasl.oauthbearer.client.id'] = config.sasl_oauthbearer_client_id
|
|
140
|
+
if config.sasl_oauthbearer_client_secret:
|
|
141
|
+
producer_config['sasl.oauthbearer.client.secret'] = config.sasl_oauthbearer_client_secret
|
|
142
|
+
if config.sasl_oauthbearer_token_endpoint_url:
|
|
143
|
+
producer_config['sasl.oauthbearer.token.endpoint.url'] = config.sasl_oauthbearer_token_endpoint_url
|
|
144
|
+
if config.ssl_ca_location:
|
|
145
|
+
producer_config['ssl.ca.location'] = config.ssl_ca_location
|
|
146
|
+
|
|
147
|
+
while True:
|
|
148
|
+
try:
|
|
149
|
+
# Create the producer - it will handle connection and authentication
|
|
150
|
+
producer = Producer(producer_config)
|
|
151
|
+
logger.info('Successfully created Kafka producer')
|
|
152
|
+
return producer
|
|
153
|
+
|
|
154
|
+
except KafkaException as e:
|
|
155
|
+
if _retries < config.max_kafka_connection_retries:
|
|
156
|
+
_retries += 1
|
|
157
|
+
logger.exception(e)
|
|
158
|
+
logger.warning(f'Retrying connecting to bootstrap servers '
|
|
159
|
+
f'[retry {_retries}/{config.max_kafka_connection_retries}]')
|
|
160
|
+
time.sleep(min(2 ** _retries, 30)) # Exponential backoff
|
|
161
|
+
else:
|
|
162
|
+
logger.error(f'Failed to connect to Kafka after {config.max_kafka_connection_retries} retries')
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
def _is_root_complete(self):
|
|
166
|
+
return self.root_task['is_complete']
|
|
167
|
+
|
|
168
|
+
def _update_root_task(self, event):
|
|
169
|
+
if (
|
|
170
|
+
event.get('type') == 'task-received'
|
|
171
|
+
and 'root_id' in event
|
|
172
|
+
and self.root_task['uuid'] is None
|
|
173
|
+
):
|
|
174
|
+
self.root_task['uuid'] = event['root_id']
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
event['uuid'] == self.root_task['uuid']
|
|
178
|
+
# crazy things can happen with the celery task state model;
|
|
179
|
+
# avoid switching out of completed.
|
|
180
|
+
and RunStates.is_complete_state(event.get('type'))
|
|
181
|
+
):
|
|
182
|
+
self.root_task['is_complete'] = True
|
|
183
|
+
|
|
184
|
+
def _send_celery_event_to_kafka(self, celery_event: dict[str, Any]) -> list[dict[str, Any]]:
|
|
185
|
+
raise NotImplementedError("Subclasses must implement sending.")
|
|
186
|
+
|
|
187
|
+
def _on_celery_event(self, event):
|
|
188
|
+
if 'uuid' not in event:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
self._update_root_task(event)
|
|
192
|
+
|
|
193
|
+
sent_kafka_events = self._send_celery_event_to_kafka(event)
|
|
194
|
+
|
|
195
|
+
if sent_kafka_events and self.recording_file:
|
|
196
|
+
# Append the event to the recording file.
|
|
197
|
+
with open(self.recording_file, "a") as rec:
|
|
198
|
+
for e in sent_kafka_events:
|
|
199
|
+
event_data_str = json.dumps(e, sort_keys=True, indent=2)
|
|
200
|
+
rec.write(event_data_str + KAFKA_EVENTS_FILE_DELIMITER)
|
|
201
|
+
|
|
202
|
+
def _on_cleanup(self):
|
|
203
|
+
"""Flush and close the producer with proper timeout handling."""
|
|
204
|
+
logger.info('Flushing Kafka producer...')
|
|
205
|
+
|
|
206
|
+
# Flush with timeout - returns number of messages still in queue
|
|
207
|
+
remaining = self.producer.flush(timeout=120)
|
|
208
|
+
|
|
209
|
+
if remaining > 0:
|
|
210
|
+
logger.warning(f'{remaining} messages were not delivered before timeout')
|
|
211
|
+
else:
|
|
212
|
+
logger.info('All messages successfully delivered to Kafka')
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class BlazeKafkaSenderThread(KafkaSenderThread):
|
|
216
|
+
"""Captures Celery events and puts them on a Kafka bus."""
|
|
217
|
+
|
|
218
|
+
def __init__(self,
|
|
219
|
+
celery_app,
|
|
220
|
+
run_metadata: FireXRunMetadata,
|
|
221
|
+
config: BlazeSenderConfig,
|
|
222
|
+
logs_url: str,
|
|
223
|
+
max_retry_attempts: Optional[int] = None,
|
|
224
|
+
receiver_ready_file: Optional[str] = None,
|
|
225
|
+
recording_file: Optional[str] = None,
|
|
226
|
+
):
|
|
227
|
+
|
|
228
|
+
super().__init__(
|
|
229
|
+
celery_app, run_metadata, config, max_retry_attempts,
|
|
230
|
+
receiver_ready_file, recording_file)
|
|
231
|
+
|
|
232
|
+
self.submitter = getuser()
|
|
233
|
+
self.firex_requester = run_metadata.firex_requester
|
|
234
|
+
self.firex_id = run_metadata.firex_id
|
|
235
|
+
self.logs_url = logs_url
|
|
236
|
+
self.kafka_topic = config.kafka_topic
|
|
237
|
+
self.uuid_to_task_name_mapping : dict[str, str] = {}
|
|
238
|
+
|
|
239
|
+
def _get_kafka_event(self, event: dict[str, Any]) -> dict[str, Any]:
|
|
240
|
+
uuid = event.pop('uuid')
|
|
241
|
+
if uuid not in self.uuid_to_task_name_mapping and 'long_name' in event:
|
|
242
|
+
self.uuid_to_task_name_mapping[uuid] = event['long_name']
|
|
243
|
+
|
|
244
|
+
if uuid in self.uuid_to_task_name_mapping:
|
|
245
|
+
task_name = self.uuid_to_task_name_mapping[uuid]
|
|
246
|
+
else:
|
|
247
|
+
# No need to produce this event since it won't be processed by Lumens anyways
|
|
248
|
+
raise NoNameForEvent(f'No task name found for {event}; can not send the event')
|
|
249
|
+
|
|
250
|
+
# Remove result since we only should report firex_result, not the native result
|
|
251
|
+
event.pop('result', None)
|
|
252
|
+
|
|
253
|
+
basic_event_data = get_basic_event(
|
|
254
|
+
name=task_name,
|
|
255
|
+
event_type=event.get('type'),
|
|
256
|
+
timestamp=event['timestamp'],
|
|
257
|
+
# Add the event_timestamp (copy of the local_received), since the native timestamp that
|
|
258
|
+
# Celery provides is broken (its local time instead of UTC, and utcoffset is inaccurate).
|
|
259
|
+
# This piece of -redundant- data is just because Lumens can't make local_received query-able
|
|
260
|
+
event_timestamp=event['local_received'])
|
|
261
|
+
|
|
262
|
+
return format_kafka_message(
|
|
263
|
+
firex_id=self.firex_id,
|
|
264
|
+
event_data=event | basic_event_data,
|
|
265
|
+
uuid=uuid,
|
|
266
|
+
logs_url=self.logs_url,
|
|
267
|
+
submitter=self.submitter,
|
|
268
|
+
firex_requester=self.firex_requester)
|
|
269
|
+
|
|
270
|
+
def _send_celery_event_to_kafka(self, celery_event: dict[str, Any]) -> list[dict[str, Any]]:
|
|
271
|
+
if celery_event.get('type') in BLAZE_SEND_EVENT_TYPES:
|
|
272
|
+
try:
|
|
273
|
+
kafka_event = self._get_kafka_event(celery_event)
|
|
274
|
+
except NoNameForEvent as e:
|
|
275
|
+
logger.exception(e)
|
|
276
|
+
else:
|
|
277
|
+
send_kafka_mssg(kafka_producer=self.producer,
|
|
278
|
+
kafka_mssg=kafka_event,
|
|
279
|
+
kafka_topic=self.kafka_topic,
|
|
280
|
+
firex_id=self.firex_id,
|
|
281
|
+
partition=self.partition)
|
|
282
|
+
return [kafka_event]
|
|
283
|
+
|
|
284
|
+
return []
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for the firex_blaze package.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from celery.app.base import Celery
|
|
10
|
+
|
|
11
|
+
from firexapp.events.event_aggregator import FireXEventAggregator
|
|
12
|
+
from firexapp.broker_manager.broker_factory import RedisManager
|
|
13
|
+
from firex_blaze.fast_blaze_helper import get_blaze_dir
|
|
14
|
+
|
|
15
|
+
KAFKA_EVENTS_FILE_DELIMITER = '--END_OF_EVENT--'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class BlazeSenderConfig:
|
|
20
|
+
kafka_topic: str
|
|
21
|
+
kafka_bootstrap_servers: list[str]
|
|
22
|
+
max_kafka_connection_retries: int
|
|
23
|
+
security_protocol: str = 'PLAINTEXT'
|
|
24
|
+
# SASL-SSL OAuth 2.0 parameters
|
|
25
|
+
sasl_mechanism: Optional[str] = None
|
|
26
|
+
sasl_oauthbearer_method: Optional[str] = None
|
|
27
|
+
sasl_oauthbearer_client_id: Optional[str] = None
|
|
28
|
+
sasl_oauthbearer_client_secret: Optional[str] = None
|
|
29
|
+
sasl_oauthbearer_token_endpoint_url: Optional[str] = None
|
|
30
|
+
ssl_ca_location: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_blaze_events_file(logs_dir, instance_name=None):
|
|
34
|
+
return os.path.join(get_blaze_dir(logs_dir, instance_name), 'kafka_events.json')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_kafka_events(logs_dir, instance_name=None):
|
|
38
|
+
import gzip
|
|
39
|
+
real_rec = os.path.realpath(get_blaze_events_file(logs_dir, instance_name))
|
|
40
|
+
if real_rec.endswith('.gz'):
|
|
41
|
+
with gzip.open(real_rec, 'rt', encoding='utf-8') as rec:
|
|
42
|
+
all_text = rec.read()
|
|
43
|
+
else:
|
|
44
|
+
with open(real_rec) as rec:
|
|
45
|
+
all_text = rec.read()
|
|
46
|
+
event_records = all_text.split(sep=KAFKA_EVENTS_FILE_DELIMITER)
|
|
47
|
+
return [
|
|
48
|
+
json.loads(e) for e in event_records
|
|
49
|
+
if e
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def aggregate_blaze_kafka_msgs(firex_id, kafka_msgs):
|
|
54
|
+
event_aggregator = FireXEventAggregator()
|
|
55
|
+
for kafka_event in kafka_msgs:
|
|
56
|
+
if kafka_event['FIREX_ID'] == firex_id:
|
|
57
|
+
inner_event = kafka_event['EVENTS'][0]
|
|
58
|
+
celery_event = dict(inner_event['DATA'])
|
|
59
|
+
celery_event['uuid'] = inner_event['UUID']
|
|
60
|
+
event_aggregator.aggregate_events([celery_event])
|
|
61
|
+
|
|
62
|
+
return event_aggregator.tasks_by_uuid
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def celery_app_from_logs_dir(logs_dir):
|
|
66
|
+
return Celery(broker=RedisManager.get_broker_url_from_logs_dir(logs_dir),
|
|
67
|
+
accept_content=['pickle', 'json'])
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from psutil import Process, TimeoutExpired
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from firexapp.submit.install_configs import FireXInstallConfigs
|
|
7
|
+
from firexapp.submit.submit import OptionalBoolean
|
|
8
|
+
from firexapp.submit.tracking_service import TrackingService
|
|
9
|
+
from firexapp.common import qualify_firex_bin, select_env_vars
|
|
10
|
+
from firexapp.submit.console import setup_console_logging
|
|
11
|
+
|
|
12
|
+
from firex_blaze.fast_blaze_helper import get_blaze_dir
|
|
13
|
+
from firexapp.discovery import PkgVersionInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = setup_console_logging(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FireXBlazeLauncher(TrackingService):
|
|
20
|
+
|
|
21
|
+
instance_name = 'blaze'
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.broker_recv_ready_file = None
|
|
25
|
+
self.is_ready_for_tasks = False
|
|
26
|
+
self.stdout_file = None
|
|
27
|
+
self.start_time = None
|
|
28
|
+
|
|
29
|
+
def extra_cli_arguments(self, arg_parser):
|
|
30
|
+
|
|
31
|
+
arg_parser.add_argument('--disable_blaze', '-disable_blaze',
|
|
32
|
+
help='Disable blaze data collection', default=None, const=True, nargs='?',
|
|
33
|
+
action=OptionalBoolean)
|
|
34
|
+
|
|
35
|
+
arg_parser.add_argument('--blaze_logs_url',
|
|
36
|
+
help='Server URL from which logs can be fetched.',
|
|
37
|
+
default=None)
|
|
38
|
+
|
|
39
|
+
# TODO: consider sensible default values, or not launching subprocess when these required args aren't supplied.
|
|
40
|
+
arg_parser.add_argument('--blaze_kafka_topic',
|
|
41
|
+
help="Topic used for Blaze's Kafka bus",
|
|
42
|
+
default=None)
|
|
43
|
+
|
|
44
|
+
arg_parser.add_argument('--blaze_bootstrap_servers',
|
|
45
|
+
help='Comma separated list of Kafka bootrap servers.',
|
|
46
|
+
default=None)
|
|
47
|
+
|
|
48
|
+
arg_parser.add_argument('--blaze_security_protocol',
|
|
49
|
+
help='Protocol used to communicate with brokers. '
|
|
50
|
+
'Valid values are: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL.',
|
|
51
|
+
default='PLAINTEXT')
|
|
52
|
+
|
|
53
|
+
# SASL-SSL OAuth 2.0 arguments
|
|
54
|
+
arg_parser.add_argument('--blaze_sasl_mechanism',
|
|
55
|
+
help='SASL mechanism to use (e.g., OAUTHBEARER).')
|
|
56
|
+
arg_parser.add_argument('--blaze_sasl_oauthbearer_method',
|
|
57
|
+
help='OAuth bearer method (e.g., oidc).')
|
|
58
|
+
arg_parser.add_argument('--blaze_sasl_oauthbearer_client_id',
|
|
59
|
+
help='OAuth client ID.')
|
|
60
|
+
arg_parser.add_argument('--blaze_sasl_oauthbearer_client_secret',
|
|
61
|
+
help='OAuth client secret.')
|
|
62
|
+
arg_parser.add_argument('--blaze_sasl_oauthbearer_token_endpoint_url',
|
|
63
|
+
help='OAuth token endpoint URL.')
|
|
64
|
+
arg_parser.add_argument('--blaze_ssl_ca_location',
|
|
65
|
+
help='CA certificate location for SSL verification.')
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def _create_blaze_command(cls, uid, args, broker_recv_ready_file):
|
|
69
|
+
cmd = [qualify_firex_bin("firex_blaze"),
|
|
70
|
+
"--uid", str(uid),
|
|
71
|
+
"--firex_requester", uid.firex_requester,
|
|
72
|
+
"--logs_dir", uid.logs_dir,
|
|
73
|
+
"--broker_recv_ready_file", broker_recv_ready_file,
|
|
74
|
+
'--logs_url', uid.logs_url,
|
|
75
|
+
'--kafka_topic', args.blaze_kafka_topic,
|
|
76
|
+
'--bootstrap_servers', args.blaze_bootstrap_servers,
|
|
77
|
+
'--instance_name', cls.instance_name,
|
|
78
|
+
'--security_protocol', args.blaze_security_protocol]
|
|
79
|
+
|
|
80
|
+
# SASL-SSL OAuth arguments
|
|
81
|
+
if hasattr(args, 'blaze_sasl_mechanism') and args.blaze_sasl_mechanism:
|
|
82
|
+
cmd += ['--sasl_mechanism', args.blaze_sasl_mechanism]
|
|
83
|
+
if hasattr(args, 'blaze_sasl_oauthbearer_method') and args.blaze_sasl_oauthbearer_method:
|
|
84
|
+
cmd += ['--sasl_oauthbearer_method', args.blaze_sasl_oauthbearer_method]
|
|
85
|
+
if hasattr(args, 'blaze_sasl_oauthbearer_client_id') and args.blaze_sasl_oauthbearer_client_id:
|
|
86
|
+
cmd += ['--sasl_oauthbearer_client_id', args.blaze_sasl_oauthbearer_client_id]
|
|
87
|
+
if hasattr(args, 'blaze_sasl_oauthbearer_client_secret') and args.blaze_sasl_oauthbearer_client_secret:
|
|
88
|
+
cmd += ['--sasl_oauthbearer_client_secret', args.blaze_sasl_oauthbearer_client_secret]
|
|
89
|
+
if hasattr(args, 'blaze_sasl_oauthbearer_token_endpoint_url') and args.blaze_sasl_oauthbearer_token_endpoint_url:
|
|
90
|
+
cmd += ['--sasl_oauthbearer_token_endpoint_url', args.blaze_sasl_oauthbearer_token_endpoint_url]
|
|
91
|
+
if hasattr(args, 'blaze_ssl_ca_location') and args.blaze_ssl_ca_location:
|
|
92
|
+
cmd += ['--ssl_ca_location', args.blaze_ssl_ca_location]
|
|
93
|
+
|
|
94
|
+
return cmd
|
|
95
|
+
|
|
96
|
+
def start(self, args, install_configs: FireXInstallConfigs, uid=None, **kwargs) -> {}:
|
|
97
|
+
super().start(args, install_configs, uid=uid, **kwargs)
|
|
98
|
+
sufficient_args = uid.logs_url and args.blaze_kafka_topic and args.blaze_bootstrap_servers
|
|
99
|
+
if args.disable_blaze or not sufficient_args:
|
|
100
|
+
if args.disable_blaze:
|
|
101
|
+
logger.debug("Blaze disabled; will not launch subprocess.")
|
|
102
|
+
if not sufficient_args:
|
|
103
|
+
logger.warning("Blaze did not receive sufficient arguments; will not launch subprocess.")
|
|
104
|
+
self.is_ready_for_tasks = True
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
blaze_debug_dir = get_blaze_dir(uid.logs_dir, instance_name=self.instance_name)
|
|
108
|
+
os.makedirs(blaze_debug_dir, exist_ok=True)
|
|
109
|
+
self.broker_recv_ready_file = os.path.join(blaze_debug_dir, 'blaze_celery_recvr_ready')
|
|
110
|
+
self.stdout_file = os.path.join(blaze_debug_dir, 'blaze.stdout')
|
|
111
|
+
|
|
112
|
+
self.start_time = time.time()
|
|
113
|
+
with open(self.stdout_file, 'w+') as f:
|
|
114
|
+
pid = subprocess.Popen(
|
|
115
|
+
self._create_blaze_command(uid, args, self.broker_recv_ready_file),
|
|
116
|
+
stdout=f,
|
|
117
|
+
stderr=subprocess.STDOUT,
|
|
118
|
+
close_fds=True,
|
|
119
|
+
env=select_env_vars(['PATH']),
|
|
120
|
+
cwd=blaze_debug_dir,
|
|
121
|
+
).pid
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
Process(pid).wait(0.1)
|
|
125
|
+
except TimeoutExpired:
|
|
126
|
+
logger.debug("Started background FireXBlaze with pid %s" % pid)
|
|
127
|
+
else:
|
|
128
|
+
logger.error("Failed to start FireXBlaze -- task data will not be put on Kafka bus.")
|
|
129
|
+
|
|
130
|
+
return {}
|
|
131
|
+
|
|
132
|
+
def ready_for_tasks(self, **kwargs) -> bool:
|
|
133
|
+
if not self.is_ready_for_tasks:
|
|
134
|
+
self.is_ready_for_tasks = os.path.isfile(self.broker_recv_ready_file)
|
|
135
|
+
if self.is_ready_for_tasks:
|
|
136
|
+
logger.debug("Blaze up after %.2f s" % (time.time() - self.start_time))
|
|
137
|
+
|
|
138
|
+
return self.is_ready_for_tasks
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def get_pkg_version_info() -> PkgVersionInfo:
|
|
142
|
+
import firex_blaze
|
|
143
|
+
return PkgVersionInfo(pkg='firex-blaze',
|
|
144
|
+
version=firex_blaze.__version__)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# LOAD NO SLOW MODULES HERE!
|
|
2
|
+
# This is to keep loading the launcher module fast.
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from firexapp.submit.uid import Uid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_blaze_dir(logs_dir, instance_name=None):
|
|
9
|
+
if instance_name is None:
|
|
10
|
+
instance_name = 'blaze'
|
|
11
|
+
return os.path.join(logs_dir, Uid.debug_dirname, instance_name)
|