assemblyline-core 4.7.5.dev52__tar.gz → 4.7.5.dev56__tar.gz
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.
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/PKG-INFO +1 -1
- assemblyline_core-4.7.5.dev56/assemblyline_core/VERSION +1 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/__init__.py +1 -0
- assemblyline_core-4.7.5.dev56/assemblyline_core/scaler/base.py +327 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/scaler/collection.py +4 -3
- assemblyline_core-4.7.5.dev52/assemblyline_core/scaler/controllers/kubernetes_ctl.py → assemblyline_core-4.7.5.dev56/assemblyline_core/scaler/controllers/kubernetes.py +174 -412
- assemblyline_core-4.7.5.dev56/assemblyline_core/scaler/controllers/kubernetes_ctl.py +391 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/scaler/scaler_server.py +46 -323
- assemblyline_core-4.7.5.dev56/assemblyline_core/scaler_hpa/kubernetes.py +381 -0
- assemblyline_core-4.7.5.dev56/assemblyline_core/scaler_hpa/run_scaler.py +7 -0
- assemblyline_core-4.7.5.dev56/assemblyline_core/scaler_hpa/scaler_server.py +522 -0
- assemblyline_core-4.7.5.dev56/assemblyline_core/workflow/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core.egg-info/PKG-INFO +1 -1
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core.egg-info/SOURCES.txt +6 -2
- assemblyline_core-4.7.5.dev52/assemblyline_core/VERSION +0 -1
- assemblyline_core-4.7.5.dev52/assemblyline_core/tasking_client.py +0 -537
- assemblyline_core-4.7.5.dev52/test/test_tasking_client.py +0 -77
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/LICENCE.md +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/README.md +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/alerter/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/alerter/processing.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/alerter/run_alerter.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/archiver/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/archiver/run_archiver.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/badlist_client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/dispatching/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/dispatching/client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/dispatching/dispatcher.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/dispatching/schedules.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/expiry/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/expiry/run_expiry.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/ingester/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/ingester/constants.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/ingester/ingester.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/es_metrics.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/heartbeat_formatter.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/helper.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/metrics_server.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/run_heartbeat_manager.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/run_metrics_aggregator.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/metrics/run_statistics_aggregator.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/creator/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/creator/run.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/creator/run_worker.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/loader/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/loader/run.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/loader/run_worker.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/replay/replay.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/safelist_client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/scaler/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/scaler/controllers/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/scaler/controllers/docker_ctl.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/scaler/controllers/interface.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/scaler/run_scaler.py +0 -0
- {assemblyline_core-4.7.5.dev52/assemblyline_core/updater → assemblyline_core-4.7.5.dev56/assemblyline_core/scaler_hpa}/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/server_base.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/signature_client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/submission_client.py +0 -0
- {assemblyline_core-4.7.5.dev52/assemblyline_core/vacuum → assemblyline_core-4.7.5.dev56/assemblyline_core/updater}/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/updater/helper.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/updater/run_updater.py +0 -0
- {assemblyline_core-4.7.5.dev52/assemblyline_core/workflow → assemblyline_core-4.7.5.dev56/assemblyline_core/vacuum}/__init__.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/vacuum/crawler.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/vacuum/department_map.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/vacuum/safelist.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/vacuum/stream_map.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/vacuum/worker.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core/workflow/run_workflow.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core.egg-info/dependency_links.txt +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core.egg-info/requires.txt +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/assemblyline_core.egg-info/top_level.txt +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/setup.cfg +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/setup.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_alerter.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_badlist_client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_expiry.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_replay.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_safelist_client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_scaler.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_scheduler.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_signature_client.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_updater_helper.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_vacuum.py +0 -0
- {assemblyline_core-4.7.5.dev52 → assemblyline_core-4.7.5.dev56}/test/test_workflow.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.7.5.dev56
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import threading
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
import functools
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
|
|
9
|
+
import elasticapm
|
|
10
|
+
|
|
11
|
+
from assemblyline.common.constants import (
|
|
12
|
+
SCALER_TIMEOUT_QUEUE,
|
|
13
|
+
SERVICE_STATE_HASH,
|
|
14
|
+
ServiceStatus,
|
|
15
|
+
)
|
|
16
|
+
from assemblyline.common.forge import get_apm_client
|
|
17
|
+
from assemblyline.remote.datatypes.exporting_counter import export_metrics_once
|
|
18
|
+
from assemblyline.odm.messages.changes import Operation, ServiceChange
|
|
19
|
+
from assemblyline.odm.messages.scaler_heartbeat import Metrics
|
|
20
|
+
from assemblyline.odm.messages.scaler_status_heartbeat import Status
|
|
21
|
+
from assemblyline.remote.datatypes.events import EventSender, EventWatcher
|
|
22
|
+
from assemblyline.remote.datatypes.hash import ExpiringHash, Hash
|
|
23
|
+
from assemblyline.remote.datatypes.queues.named import NamedQueue
|
|
24
|
+
from assemblyline_core.scaler.controllers.interface import ControllerInterface, ServiceControlError
|
|
25
|
+
from assemblyline_core.server_base import ServiceStage, ThreadedCoreBase
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# How often (in seconds) to download new service data, try to scale managed services,
|
|
29
|
+
# and download more metrics data respectively
|
|
30
|
+
SERVICE_SYNC_INTERVAL = 60 * 30 # Every half hour
|
|
31
|
+
HEARTBEAT_INTERVAL = 5
|
|
32
|
+
|
|
33
|
+
CONTAINER_EVENTS_LOG_INTERVAL = 2
|
|
34
|
+
HOSTNAME = os.getenv('HOSTNAME', platform.node())
|
|
35
|
+
APM_SPAN_TYPE = 'scaler'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@contextmanager
|
|
39
|
+
def apm_span(client: Optional[elasticapm.Client], span_name: str):
|
|
40
|
+
try:
|
|
41
|
+
if client:
|
|
42
|
+
client.begin_transaction(APM_SPAN_TYPE)
|
|
43
|
+
yield None
|
|
44
|
+
if client:
|
|
45
|
+
client.end_transaction(span_name, 'success')
|
|
46
|
+
except Exception:
|
|
47
|
+
if client:
|
|
48
|
+
client.end_transaction(span_name, 'exception')
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProfileBase:
|
|
53
|
+
def __init__(self, name: str, target_queue_length: int) -> None:
|
|
54
|
+
self.name = name
|
|
55
|
+
# How long does a backlog need to be before we are concerned
|
|
56
|
+
self.target_queue_length = target_queue_length
|
|
57
|
+
|
|
58
|
+
# Number of instances kubernetes expects to be running
|
|
59
|
+
self.target_instances: int = 0
|
|
60
|
+
|
|
61
|
+
# The number of instances running based on feedback metrics
|
|
62
|
+
self.running_instances: int = 0
|
|
63
|
+
|
|
64
|
+
# Last reported length of the service task queue
|
|
65
|
+
self.queue_length: int = 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ScalerBase(ThreadedCoreBase):
|
|
69
|
+
"""Common scaler methods."""
|
|
70
|
+
def __init__(self, config=None, datastore=None, redis=None, redis_persist=None) -> None:
|
|
71
|
+
super().__init__('assemblyline.scaler', config=config, datastore=datastore,
|
|
72
|
+
redis=redis, redis_persist=redis_persist)
|
|
73
|
+
|
|
74
|
+
self.scaler_timeout_queue: NamedQueue[dict] = NamedQueue(SCALER_TIMEOUT_QUEUE, host=self.redis_persist)
|
|
75
|
+
self.status_table = ExpiringHash(SERVICE_STATE_HASH, host=self.redis, ttl=30*60)
|
|
76
|
+
self.service_event_sender: EventSender[dict] = EventSender('changes.services', host=self.redis)
|
|
77
|
+
self.service_watcher_wakeup = threading.Event()
|
|
78
|
+
self.service_change_watcher = EventWatcher(self.redis, deserializer=ServiceChange.deserialize)
|
|
79
|
+
self.service_change_watcher.register('changes.services.*', self._handle_service_change_event)
|
|
80
|
+
|
|
81
|
+
self.controller: ControllerInterface
|
|
82
|
+
|
|
83
|
+
# Information about services
|
|
84
|
+
self.profiles: dict[str, ProfileBase] = {}
|
|
85
|
+
self.profiles_lock = threading.RLock()
|
|
86
|
+
|
|
87
|
+
# Load the APM connection if any
|
|
88
|
+
self.apm_client = None
|
|
89
|
+
if self.config.core.metrics.apm_server.server_url:
|
|
90
|
+
elasticapm.instrument()
|
|
91
|
+
self.apm_client = get_apm_client("scaler")
|
|
92
|
+
|
|
93
|
+
def try_run(self):
|
|
94
|
+
self.service_change_watcher.start()
|
|
95
|
+
self.maintain_threads({
|
|
96
|
+
'Log Container Events': self.log_container_events,
|
|
97
|
+
'Process Timeouts': self.process_timeouts,
|
|
98
|
+
'Service Configuration Sync': self.sync_services,
|
|
99
|
+
'Service Adjuster': self.update_scaling,
|
|
100
|
+
'Import Metrics': self.sync_metrics,
|
|
101
|
+
'Export Metrics': self.export_metrics,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
def stop(self):
|
|
105
|
+
super().stop()
|
|
106
|
+
self.service_change_watcher.stop()
|
|
107
|
+
self.service_watcher_wakeup.set()
|
|
108
|
+
self.controller.stop()
|
|
109
|
+
|
|
110
|
+
def _handle_service_change_event(self, data: Optional[ServiceChange]):
|
|
111
|
+
if data is None:
|
|
112
|
+
self.service_watcher_wakeup.set()
|
|
113
|
+
else:
|
|
114
|
+
if data.operation == Operation.Removed:
|
|
115
|
+
self.log.info(f'Service appears to be deleted, removing {data.name}')
|
|
116
|
+
stage = self.get_service_stage(data.name)
|
|
117
|
+
self.stop_service(data.name, stage)
|
|
118
|
+
elif data.operation == Operation.Incompatible:
|
|
119
|
+
return
|
|
120
|
+
else:
|
|
121
|
+
service = self.datastore.get_service_with_delta(data.name)
|
|
122
|
+
if not service:
|
|
123
|
+
self.log.warning(f'Received change event for non-existent service: {data.name}. Ignoring..')
|
|
124
|
+
return
|
|
125
|
+
self._sync_service(service)
|
|
126
|
+
|
|
127
|
+
def sync_services(self) -> None:
|
|
128
|
+
last_synced_profiles = None
|
|
129
|
+
while self.running:
|
|
130
|
+
with apm_span(self.apm_client, 'sync_services'):
|
|
131
|
+
self.log.info('Synchronizing service configuration')
|
|
132
|
+
with self.profiles_lock:
|
|
133
|
+
current_services = set(self.profiles.keys())
|
|
134
|
+
|
|
135
|
+
# Check to see if the service is progressing since it's last sync
|
|
136
|
+
if last_synced_profiles:
|
|
137
|
+
for service, profile in self.profiles.items():
|
|
138
|
+
# Assume there was no backlog initially if the service is new since last sync
|
|
139
|
+
last_synced_backlog = 0
|
|
140
|
+
if last_synced_profiles.get(service):
|
|
141
|
+
last_synced_backlog = last_synced_profiles[service].queue_length
|
|
142
|
+
|
|
143
|
+
# Check to see if the backlog has increased and if the service has been running since
|
|
144
|
+
backlog = profile.queue_length
|
|
145
|
+
trying_to_start = profile.running_instances == 0 and profile.target_instances > 0
|
|
146
|
+
if backlog and backlog >= last_synced_backlog and trying_to_start:
|
|
147
|
+
# Restart the service in an attempt to resolve intermittent issues with container/pod
|
|
148
|
+
self.controller.restart(profile)
|
|
149
|
+
|
|
150
|
+
# Update the last synced profiles for next time
|
|
151
|
+
last_synced_profiles = self.profiles
|
|
152
|
+
|
|
153
|
+
discovered_services: list[str] = []
|
|
154
|
+
|
|
155
|
+
# Get all the service data
|
|
156
|
+
for service in self.datastore.list_all_services(full=True):
|
|
157
|
+
self._sync_service(service)
|
|
158
|
+
discovered_services.append(service.name)
|
|
159
|
+
|
|
160
|
+
# Find any services we have running, that are no longer in the database and remove them
|
|
161
|
+
for stray_service in current_services - set(discovered_services):
|
|
162
|
+
self.log.info('Service appears to be deleted, removing stray %s', stray_service)
|
|
163
|
+
stage = self.get_service_stage(stray_service)
|
|
164
|
+
self.stop_service(stray_service, stage)
|
|
165
|
+
self.log.info('Finish synchronizing service configuration')
|
|
166
|
+
|
|
167
|
+
# Wait for the interval or until someone wakes us up
|
|
168
|
+
self.service_watcher_wakeup.wait(timeout=SERVICE_SYNC_INTERVAL)
|
|
169
|
+
self.service_watcher_wakeup.clear()
|
|
170
|
+
|
|
171
|
+
def _sync_service(self, service):
|
|
172
|
+
raise NotImplementedError()
|
|
173
|
+
|
|
174
|
+
@elasticapm.capture_span(span_type=APM_SPAN_TYPE)
|
|
175
|
+
def stop_service(self, name: str, current_stage: ServiceStage):
|
|
176
|
+
if current_stage != ServiceStage.Off:
|
|
177
|
+
# Disable this service's dependencies
|
|
178
|
+
self.controller.stop_containers(labels={
|
|
179
|
+
'dependency_for': name
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
# Clear related dependency caching from Redis
|
|
183
|
+
Hash(f'service-updates-{name}', self.redis_persist).delete()
|
|
184
|
+
|
|
185
|
+
# Mark this service as not running in the shared record
|
|
186
|
+
self._service_stage_hash.set(name, ServiceStage.Off)
|
|
187
|
+
|
|
188
|
+
# Stop any running disabled services
|
|
189
|
+
if name in self.profiles or self.controller.get_target(name) > 0:
|
|
190
|
+
self.log.info(f'Removing {name} from scaling')
|
|
191
|
+
with self.profiles_lock:
|
|
192
|
+
self.profiles.pop(name, None)
|
|
193
|
+
self.controller.set_target(name, 0)
|
|
194
|
+
|
|
195
|
+
def log_crashes(self, fn):
|
|
196
|
+
@functools.wraps(fn)
|
|
197
|
+
def with_logs(*args, **kwargs):
|
|
198
|
+
# noinspection PyBroadException
|
|
199
|
+
try:
|
|
200
|
+
fn(*args, **kwargs)
|
|
201
|
+
except ServiceControlError as error:
|
|
202
|
+
self.log.exception(f"Error while managing service: {error.service_name}")
|
|
203
|
+
except Exception:
|
|
204
|
+
self.log.exception(f'Crash in scaler: {fn.__name__}')
|
|
205
|
+
return with_logs
|
|
206
|
+
|
|
207
|
+
def _timeout_kill(self, service, container):
|
|
208
|
+
with apm_span(self.apm_client, 'timeout_kill'):
|
|
209
|
+
self.controller.stop_container(service, container)
|
|
210
|
+
self.status_table.pop(container)
|
|
211
|
+
|
|
212
|
+
def process_timeouts(self):
|
|
213
|
+
with concurrent.futures.ThreadPoolExecutor(10) as pool:
|
|
214
|
+
futures = []
|
|
215
|
+
|
|
216
|
+
while self.running:
|
|
217
|
+
message = self.scaler_timeout_queue.pop(blocking=True, timeout=1)
|
|
218
|
+
if not message:
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
with apm_span(self.apm_client, 'process_timeouts'):
|
|
222
|
+
# Process new messages
|
|
223
|
+
self.log.info(f"Killing service container: {message['container']} running: {message['service']}")
|
|
224
|
+
futures.append(pool.submit(self._timeout_kill, message['service'], message['container']))
|
|
225
|
+
|
|
226
|
+
# Process finished
|
|
227
|
+
finished = [_f for _f in futures if _f.done()]
|
|
228
|
+
futures = [_f for _f in futures if _f not in finished]
|
|
229
|
+
for _f in finished:
|
|
230
|
+
exception = _f.exception()
|
|
231
|
+
if exception is not None:
|
|
232
|
+
self.log.error("Exception trying to stop timed out service container: %s", exception)
|
|
233
|
+
|
|
234
|
+
def get_cpu_overallocation(self) -> float:
|
|
235
|
+
node_limit = self.config.core.scaler.overallocation_node_limit
|
|
236
|
+
if node_limit is not None and node_limit <= self.controller.node_count:
|
|
237
|
+
return 1
|
|
238
|
+
return self.config.core.scaler.cpu_overallocation
|
|
239
|
+
|
|
240
|
+
def get_memory_overallocation(self) -> float:
|
|
241
|
+
node_limit = self.config.core.scaler.overallocation_node_limit
|
|
242
|
+
if node_limit is not None and node_limit <= self.controller.node_count:
|
|
243
|
+
return 1
|
|
244
|
+
return self.config.core.scaler.memory_overallocation
|
|
245
|
+
|
|
246
|
+
def get_cpu_info(self, overallocation: bool) -> tuple[float, float]:
|
|
247
|
+
# Get the raw used resource numbers
|
|
248
|
+
free_cpu, total_cpu = self.controller.cpu_info()
|
|
249
|
+
|
|
250
|
+
# Recalculate the amount of free resources expanding the total quantity by the overallocation
|
|
251
|
+
if overallocation:
|
|
252
|
+
used_cpu = total_cpu - free_cpu
|
|
253
|
+
free_cpu = total_cpu * self.get_cpu_overallocation() - used_cpu
|
|
254
|
+
|
|
255
|
+
# Include the service containers not counted in the raw numbers because they are pending
|
|
256
|
+
for name, pending in self.controller.get_unavailable().items():
|
|
257
|
+
profile = self.profiles.get(name)
|
|
258
|
+
if not profile or not pending:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
free_cpu = free_cpu - profile.container_config.cpu_cores * pending
|
|
262
|
+
|
|
263
|
+
return (free_cpu, total_cpu)
|
|
264
|
+
|
|
265
|
+
def get_memory_info(self, overallocation: bool) -> tuple[float, float]:
|
|
266
|
+
# Get the raw used resource numbers
|
|
267
|
+
free_memory, total_memory = self.controller.memory_info()
|
|
268
|
+
|
|
269
|
+
# Recalculate the amount of free resources expanding the total quantity by the overallocation
|
|
270
|
+
if overallocation:
|
|
271
|
+
used_memory = total_memory - free_memory
|
|
272
|
+
free_memory = total_memory * self.get_memory_overallocation() - used_memory
|
|
273
|
+
|
|
274
|
+
# Include the service containers not counted in the raw numbers because they are pending
|
|
275
|
+
for name, pending in self.controller.get_unavailable().items():
|
|
276
|
+
profile = self.profiles.get(name)
|
|
277
|
+
if not profile or not pending:
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
free_memory = free_memory - profile.container_config.ram_mb * pending
|
|
281
|
+
|
|
282
|
+
return (free_memory, total_memory)
|
|
283
|
+
|
|
284
|
+
def log_container_events(self):
|
|
285
|
+
"""The service status table may have references to containers that have crashed. Try to remove them all."""
|
|
286
|
+
while self.sleep(CONTAINER_EVENTS_LOG_INTERVAL):
|
|
287
|
+
with apm_span(self.apm_client, 'log_container_events'):
|
|
288
|
+
for message in self.controller.new_events():
|
|
289
|
+
self.log.warning("Container Event :: " + message)
|
|
290
|
+
|
|
291
|
+
def export_metrics(self):
|
|
292
|
+
while self.sleep(self.config.logging.export_interval):
|
|
293
|
+
with apm_span(self.apm_client, 'export_metrics'):
|
|
294
|
+
service_metrics = {}
|
|
295
|
+
with self.profiles_lock:
|
|
296
|
+
for service_name, profile in self.profiles.items():
|
|
297
|
+
service_metrics[service_name] = {
|
|
298
|
+
'running': profile.running_instances,
|
|
299
|
+
'target': profile.target_instances,
|
|
300
|
+
'minimum': profile.min_instances,
|
|
301
|
+
'maximum': profile.instance_limit,
|
|
302
|
+
'dynamic_maximum': profile.max_instances,
|
|
303
|
+
'queue': profile.queue_length,
|
|
304
|
+
'duty_cycle': profile.duty_cycle,
|
|
305
|
+
'pressure': profile.pressure,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for service_name, metrics in service_metrics.items():
|
|
309
|
+
export_metrics_once(service_name, Status, metrics, host=HOSTNAME,
|
|
310
|
+
counter_type='scaler_status', config=self.config, redis=self.redis)
|
|
311
|
+
|
|
312
|
+
memory, memory_total = self.get_memory_info(overallocation=False)
|
|
313
|
+
cpu, cpu_total = self.get_cpu_info(overallocation=False)
|
|
314
|
+
metrics = {
|
|
315
|
+
'memory_total': memory_total,
|
|
316
|
+
'cpu_total': cpu_total,
|
|
317
|
+
'memory_free': memory,
|
|
318
|
+
'cpu_free': cpu
|
|
319
|
+
}
|
|
320
|
+
export_metrics_once('scaler', Metrics, metrics, host=HOSTNAME,
|
|
321
|
+
counter_type='scaler', config=self.config, redis=self.redis)
|
|
322
|
+
|
|
323
|
+
def update_scaling(self):
|
|
324
|
+
raise NotImplementedError()
|
|
325
|
+
|
|
326
|
+
def sync_metrics(self):
|
|
327
|
+
raise NotImplementedError()
|
|
@@ -12,10 +12,9 @@ Row = namedtuple('Row', ['timestamp', 'busy', 'throughput'])
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Collection:
|
|
15
|
-
|
|
15
|
+
"""A buffer for metrics data from multiple instances of multiple services."""
|
|
16
|
+
def __init__(self, period, ttl=None) -> None:
|
|
16
17
|
"""
|
|
17
|
-
A buffer for metrics data from multiple instances of multiple services.
|
|
18
|
-
|
|
19
18
|
:param period: Expected seconds between updates
|
|
20
19
|
:param ttl: Seconds before a message is dropped from the buffer
|
|
21
20
|
"""
|
|
@@ -24,6 +23,7 @@ class Collection:
|
|
|
24
23
|
self.services: Dict[str, Dict[str, Row]] = {}
|
|
25
24
|
|
|
26
25
|
def update(self, service, host, busy_seconds, throughput):
|
|
26
|
+
"""Store a new record for a given host."""
|
|
27
27
|
# Load the sequence of data points that
|
|
28
28
|
try:
|
|
29
29
|
hosts = self.services[service]
|
|
@@ -34,6 +34,7 @@ class Collection:
|
|
|
34
34
|
hosts[host] = Row(time.time(), busy_seconds, throughput)
|
|
35
35
|
|
|
36
36
|
def read(self, service):
|
|
37
|
+
"""Read how many instances of a service are known and how busy they are."""
|
|
37
38
|
now = time.time()
|
|
38
39
|
|
|
39
40
|
# Load the last messages from this service
|