benchlab-pytools 3.0.0__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.
- benchlab/__init__.py +14 -0
- benchlab/__main__.py +21 -0
- benchlab/config/README.md +325 -0
- benchlab/config/__init__.py +9 -0
- benchlab/config/config_client.py +631 -0
- benchlab/config/config_manager.py +495 -0
- benchlab/config/config_tool.py +358 -0
- benchlab/config/diff.py +163 -0
- benchlab/config/examples/complete_config.json +56 -0
- benchlab/config/examples/rgb_lighting.json +24 -0
- benchlab/config/examples/simple_fan.json +32 -0
- benchlab/config/schema.py +138 -0
- benchlab/core/README.md +154 -0
- benchlab/core/__init__.py +78 -0
- benchlab/core/config.py +145 -0
- benchlab/core/datasource.py +1209 -0
- benchlab/core/datasource_manager.py +418 -0
- benchlab/core/device_registry.py +206 -0
- benchlab/core/discovery.py +92 -0
- benchlab/core/infrastructure.py +309 -0
- benchlab/core/process_manager.py +356 -0
- benchlab/core/retry.py +85 -0
- benchlab/core/shared_serial.py +235 -0
- benchlab/core/statistics.py +224 -0
- benchlab/csv_log/README.md +206 -0
- benchlab/csv_log/__init__.py +5 -0
- benchlab/csv_log/csv_logger_enhanced.py +416 -0
- benchlab/csv_log/message_batcher.py +361 -0
- benchlab/csv_log/requirements.txt +3 -0
- benchlab/csv_log/smart_retry.py +466 -0
- benchlab/graph/README.md +117 -0
- benchlab/graph/__init__.py +5 -0
- benchlab/graph/app.py +353 -0
- benchlab/graph/requirements.txt +2 -0
- benchlab/graph/runner.py +42 -0
- benchlab/graph/ui.py +288 -0
- benchlab/hwinfo/README.md +135 -0
- benchlab/hwinfo/__init__.py +5 -0
- benchlab/hwinfo/hwinfo_export.py +386 -0
- benchlab/hwinfo/requirements.txt +1 -0
- benchlab/launcher.py +387 -0
- benchlab/link/.env.EXAMPLE +10 -0
- benchlab/link/README.md +172 -0
- benchlab/link/__init__.py +1 -0
- benchlab/link/link_main.py +479 -0
- benchlab/main.py +462 -0
- benchlab/menu.py +729 -0
- benchlab/menu_classic.py +410 -0
- benchlab/mqtt/README.md +201 -0
- benchlab/mqtt/__init__.py +5 -0
- benchlab/mqtt/mqtt.config_template +2 -0
- benchlab/mqtt/mqtt_publisher.py +540 -0
- benchlab/mqtt/requirements.txt +3 -0
- benchlab/prefs.py +67 -0
- benchlab/restapi/.env.example +17 -0
- benchlab/restapi/__init__.py +5 -0
- benchlab/restapi/favicon.ico +0 -0
- benchlab/restapi/readme.md +202 -0
- benchlab/restapi/requirements.txt +5 -0
- benchlab/restapi/telemetry_api.py +808 -0
- benchlab/sources.py +515 -0
- benchlab/tools.py +147 -0
- benchlab/tui/README.md +266 -0
- benchlab/tui/__init__.py +5 -0
- benchlab/tui/config.py +477 -0
- benchlab/tui/requirements.txt +2 -0
- benchlab/tui/tui_core.py +1298 -0
- benchlab/tui/tui_main.py +418 -0
- benchlab/vu/README.md +64 -0
- benchlab/vu/VU-Server/NOTICE.md +13 -0
- benchlab/vu/VU-Server/README.md +80 -0
- benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
- benchlab/vu/VU-Server/__init__.py +0 -0
- benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
- benchlab/vu/VU-Server/community_applications.md +13 -0
- benchlab/vu/VU-Server/config.yaml +9 -0
- benchlab/vu/VU-Server/database.py +256 -0
- benchlab/vu/VU-Server/dial_driver.py +534 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
- benchlab/vu/VU-Server/dials/__init__.py +0 -0
- benchlab/vu/VU-Server/dials/base_logger.py +100 -0
- benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
- benchlab/vu/VU-Server/installer/install.nsi +148 -0
- benchlab/vu/VU-Server/make_version.py +40 -0
- benchlab/vu/VU-Server/pylintrc +628 -0
- benchlab/vu/VU-Server/requirements.txt +9 -0
- benchlab/vu/VU-Server/serial_driver.py +244 -0
- benchlab/vu/VU-Server/server.py +670 -0
- benchlab/vu/VU-Server/server_config.py +255 -0
- benchlab/vu/VU-Server/server_dial_handler.py +334 -0
- benchlab/vu/VU-Server/vu_notifications.py +24 -0
- benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
- benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
- benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
- benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
- benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
- benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
- benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
- benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
- benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
- benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
- benchlab/vu/VU-Server/www/favicon.ico +0 -0
- benchlab/vu/VU-Server/www/favicon.png +0 -0
- benchlab/vu/VU-Server/www/index.html +47 -0
- benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
- benchlab/vu/VU-Server/www/views/dial.html +286 -0
- benchlab/vu/VU-Server/www/views/footer.html +27 -0
- benchlab/vu/VU-Server/www/views/header.html +86 -0
- benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
- benchlab/vu/VU-Server/www/views/start.html +124 -0
- benchlab/vu/__init__.py +5 -0
- benchlab/vu/assets/Barlow-Bold.ttf +0 -0
- benchlab/vu/assets/bl_dial_144x200.png +0 -0
- benchlab/vu/assets/bl_logo_144x200.png +0 -0
- benchlab/vu/devices.py +166 -0
- benchlab/vu/requirements.txt +24 -0
- benchlab/vu/sensors.py +18 -0
- benchlab/vu/vu_dial.config_template +90 -0
- benchlab/vu/vu_logo_gen.py +97 -0
- benchlab/vu/vu_server.config_template +5 -0
- benchlab/vu/vu_server_config.py +40 -0
- benchlab/vu/vu_server_manager.py +192 -0
- benchlab/vu/vu_tui.py +919 -0
- benchlab/vu/vu_updater.py +540 -0
- benchlab/wigidash/README.md +182 -0
- benchlab/wigidash/__init__.py +5 -0
- benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
- benchlab/wigidash/assets/Inter.ttf +0 -0
- benchlab/wigidash/assets/benchlab.png +0 -0
- benchlab/wigidash/benchlab_fleet.py +150 -0
- benchlab/wigidash/benchlab_graph.py +517 -0
- benchlab/wigidash/benchlab_overview.py +426 -0
- benchlab/wigidash/benchlab_telemetry.py +105 -0
- benchlab/wigidash/benchlab_ui.py +190 -0
- benchlab/wigidash/benchlab_utils.py +182 -0
- benchlab/wigidash/requirements.txt +9 -0
- benchlab/wigidash/wigidash_device.py +541 -0
- benchlab/wigidash/wigidash_manager.py +342 -0
- benchlab/wigidash/wigidash_session.py +355 -0
- benchlab/wigidash/wigidash_usb.py +247 -0
- benchlab/wigidash/wigidash_widget.py +88 -0
- benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
- benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
- benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
- benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
- benchlab_pytools-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message Batching System for BENCHLAB CSV Logger
|
|
3
|
+
Provides efficient buffering and batch processing for improved performance
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import json
|
|
8
|
+
import csv
|
|
9
|
+
import threading
|
|
10
|
+
from typing import List, Dict, Any, Optional, Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BatchConfig:
|
|
19
|
+
"""Configuration for message batching"""
|
|
20
|
+
batch_size: int = 100
|
|
21
|
+
flush_interval: float = 30.0 # seconds between automatic flushes
|
|
22
|
+
# maximum messages in buffer before dropping
|
|
23
|
+
max_buffer_size: int = 10000
|
|
24
|
+
flush_on_shutdown: bool = True
|
|
25
|
+
enable_metrics: bool = True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class BatchMetrics:
|
|
30
|
+
"""Metrics for batch processing performance"""
|
|
31
|
+
total_batches: int = 0
|
|
32
|
+
total_messages: int = 0
|
|
33
|
+
total_flushes: int = 0
|
|
34
|
+
avg_batch_size: float = 0.0
|
|
35
|
+
avg_flush_time: float = 0.0
|
|
36
|
+
last_flush_time: Optional[float] = None
|
|
37
|
+
buffer_utilization: float = 0.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MessageBatcher:
|
|
41
|
+
"""Efficient message batching system with configurable strategies."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, config: BatchConfig = None):
|
|
44
|
+
self.config = config or BatchConfig()
|
|
45
|
+
self.buffer: List[Dict[str, Any]] = []
|
|
46
|
+
self.buffer_lock = threading.Lock()
|
|
47
|
+
# Single lock — no separate flush_lock to avoid deadlocks between
|
|
48
|
+
# the background flush thread and manual flush() calls.
|
|
49
|
+
self.metrics = BatchMetrics()
|
|
50
|
+
self.logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
self.flush_callback: Optional[Callable] = None
|
|
53
|
+
self.metrics_callback: Optional[Callable] = None
|
|
54
|
+
|
|
55
|
+
self.shutdown_event = threading.Event()
|
|
56
|
+
self.flush_thread = threading.Thread(
|
|
57
|
+
target=self._flush_worker, daemon=True, name="BatcherFlush"
|
|
58
|
+
)
|
|
59
|
+
self.flush_thread.start()
|
|
60
|
+
|
|
61
|
+
def add_message(self, message: Dict[str, Any]) -> bool:
|
|
62
|
+
"""Add a message to the batch buffer."""
|
|
63
|
+
with self.buffer_lock:
|
|
64
|
+
if len(self.buffer) >= self.config.max_buffer_size:
|
|
65
|
+
self.logger.warning("Buffer full, dropping message")
|
|
66
|
+
return False
|
|
67
|
+
self.buffer.append(message)
|
|
68
|
+
should_flush = len(self.buffer) >= self.config.batch_size
|
|
69
|
+
|
|
70
|
+
if should_flush:
|
|
71
|
+
self.flush()
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
def flush(self) -> bool:
|
|
75
|
+
"""Flush the current buffer to the callback."""
|
|
76
|
+
with self.buffer_lock:
|
|
77
|
+
if not self.buffer:
|
|
78
|
+
return True
|
|
79
|
+
messages_to_flush = self.buffer.copy()
|
|
80
|
+
self.buffer.clear()
|
|
81
|
+
|
|
82
|
+
start_time = time.time()
|
|
83
|
+
try:
|
|
84
|
+
if self.flush_callback:
|
|
85
|
+
self.flush_callback(messages_to_flush)
|
|
86
|
+
|
|
87
|
+
elapsed = time.time() - start_time
|
|
88
|
+
self.metrics.total_batches += 1
|
|
89
|
+
self.metrics.total_messages += len(messages_to_flush)
|
|
90
|
+
self.metrics.total_flushes += 1
|
|
91
|
+
self.metrics.last_flush_time = elapsed
|
|
92
|
+
if self.metrics.total_flushes > 0:
|
|
93
|
+
n = self.metrics.total_flushes
|
|
94
|
+
self.metrics.avg_flush_time = (
|
|
95
|
+
(self.metrics.avg_flush_time * (n - 1) + elapsed) / n
|
|
96
|
+
)
|
|
97
|
+
self.logger.debug(
|
|
98
|
+
f"Flushed {
|
|
99
|
+
len(messages_to_flush)} messages in {
|
|
100
|
+
elapsed:.3f}s")
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.logger.error(f"Failed to flush batch: {e}")
|
|
105
|
+
# Restore messages to buffer on failure
|
|
106
|
+
with self.buffer_lock:
|
|
107
|
+
self.buffer.extend(messages_to_flush)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def set_flush_callback(
|
|
111
|
+
self, callback: Callable[[List[Dict[str, Any]]], None]):
|
|
112
|
+
self.flush_callback = callback
|
|
113
|
+
|
|
114
|
+
def set_metrics_callback(self, callback: Callable[[BatchMetrics], None]):
|
|
115
|
+
self.metrics_callback = callback
|
|
116
|
+
|
|
117
|
+
def get_metrics(self) -> BatchMetrics:
|
|
118
|
+
with self.buffer_lock:
|
|
119
|
+
self.metrics.buffer_utilization = len(
|
|
120
|
+
self.buffer) / self.config.max_buffer_size
|
|
121
|
+
return self.metrics
|
|
122
|
+
|
|
123
|
+
def _flush_worker(self):
|
|
124
|
+
"""Background worker for periodic flushing."""
|
|
125
|
+
while not self.shutdown_event.wait(self.config.flush_interval):
|
|
126
|
+
with self.buffer_lock:
|
|
127
|
+
has_data = bool(self.buffer)
|
|
128
|
+
if has_data:
|
|
129
|
+
self.flush()
|
|
130
|
+
|
|
131
|
+
def shutdown(self):
|
|
132
|
+
"""Shutdown the batcher and flush remaining messages."""
|
|
133
|
+
self.shutdown_event.set()
|
|
134
|
+
if self.config.flush_on_shutdown:
|
|
135
|
+
self.flush()
|
|
136
|
+
if self.flush_thread.is_alive():
|
|
137
|
+
self.flush_thread.join(timeout=5.0)
|
|
138
|
+
self.logger.info("Message batcher shutdown complete")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CSVBatchWriter:
|
|
142
|
+
"""Batch writer for CSV files."""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
output_dir: str,
|
|
147
|
+
batch_size: int = 100,
|
|
148
|
+
format: str = "csv"):
|
|
149
|
+
self.output_dir = Path(output_dir)
|
|
150
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
self.batch_size = batch_size
|
|
152
|
+
self.format = format
|
|
153
|
+
self.logger = logging.getLogger(__name__)
|
|
154
|
+
|
|
155
|
+
self.active_files: Dict[str, Dict] = {}
|
|
156
|
+
self.file_locks: Dict[str, threading.Lock] = {}
|
|
157
|
+
self.buffers: Dict[str, List[Dict[str, Any]]] = {}
|
|
158
|
+
self.buffer_locks: Dict[str, threading.Lock] = {}
|
|
159
|
+
# Fieldnames fixed at file creation (from the first message's keys).
|
|
160
|
+
# Later messages with different keys are still written safely: extra
|
|
161
|
+
# keys are dropped, missing keys are left blank — this prevents a
|
|
162
|
+
# transient schema change (e.g. a sensor briefly absent) from
|
|
163
|
+
# crash-looping the writer or corrupting the CSV.
|
|
164
|
+
self.fieldnames: Dict[str, List[str]] = {}
|
|
165
|
+
|
|
166
|
+
def write_batch(self, messages: List[Dict[str, Any]]):
|
|
167
|
+
"""Write a batch of messages, grouped by device UID."""
|
|
168
|
+
device_messages: Dict[str, List] = {}
|
|
169
|
+
for message in messages:
|
|
170
|
+
uid = message.get("uid", "unknown")
|
|
171
|
+
device_messages.setdefault(uid, []).append(message)
|
|
172
|
+
|
|
173
|
+
for uid, msgs in device_messages.items():
|
|
174
|
+
self._write_device_batch(uid, msgs)
|
|
175
|
+
|
|
176
|
+
def _write_device_batch(self, uid: str, messages: List[Dict[str, Any]]):
|
|
177
|
+
if uid not in self.active_files:
|
|
178
|
+
self._setup_device_file(uid, messages)
|
|
179
|
+
|
|
180
|
+
lock = self.buffer_locks.setdefault(uid, threading.Lock())
|
|
181
|
+
with lock:
|
|
182
|
+
self.buffers.setdefault(uid, []).extend(messages)
|
|
183
|
+
if len(self.buffers[uid]) >= self.batch_size:
|
|
184
|
+
self._flush_device_buffer(uid)
|
|
185
|
+
|
|
186
|
+
def _setup_device_file(self, uid: str, messages: List[Dict[str, Any]]):
|
|
187
|
+
"""Create the CSV file and write headers using the first message."""
|
|
188
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
189
|
+
filename = f"log_{timestamp}_{uid}.{self.format}"
|
|
190
|
+
filepath = self.output_dir / filename
|
|
191
|
+
|
|
192
|
+
self.active_files[uid] = {
|
|
193
|
+
"filepath": filepath,
|
|
194
|
+
"headers_written": False,
|
|
195
|
+
}
|
|
196
|
+
self.file_locks[uid] = threading.Lock()
|
|
197
|
+
self.buffer_locks[uid] = threading.Lock()
|
|
198
|
+
|
|
199
|
+
# Write headers immediately using the first message so the file
|
|
200
|
+
# exists on disk and is non-empty even before the first flush.
|
|
201
|
+
if messages and self.format == "csv":
|
|
202
|
+
headers = list(messages[0].keys())
|
|
203
|
+
self.fieldnames[uid] = headers
|
|
204
|
+
with open(filepath, "w", newline="", encoding="utf-8") as f:
|
|
205
|
+
csv.DictWriter(f, fieldnames=headers).writeheader()
|
|
206
|
+
self.active_files[uid]["headers_written"] = True
|
|
207
|
+
|
|
208
|
+
def _flush_device_buffer(self, uid: str):
|
|
209
|
+
"""Flush the in-memory buffer for a device to disk."""
|
|
210
|
+
buf = self.buffers.get(uid)
|
|
211
|
+
if not buf:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
messages_to_write = buf.copy()
|
|
215
|
+
buf.clear()
|
|
216
|
+
|
|
217
|
+
file_info = self.active_files[uid]
|
|
218
|
+
with self.file_locks[uid]:
|
|
219
|
+
try:
|
|
220
|
+
if self.format == "csv":
|
|
221
|
+
self._write_csv_batch(uid, file_info, messages_to_write)
|
|
222
|
+
elif self.format == "json":
|
|
223
|
+
self._write_json_batch(file_info, messages_to_write)
|
|
224
|
+
self.logger.debug(
|
|
225
|
+
f"Wrote {
|
|
226
|
+
len(messages_to_write)} rows for {uid}")
|
|
227
|
+
except Exception as e:
|
|
228
|
+
self.logger.error(f"Failed to write batch for {uid}: {e}")
|
|
229
|
+
buf.extend(messages_to_write) # restore on failure
|
|
230
|
+
|
|
231
|
+
def _write_csv_batch(self, uid: str, file_info: Dict,
|
|
232
|
+
messages: List[Dict[str, Any]]):
|
|
233
|
+
filepath = file_info["filepath"]
|
|
234
|
+
headers = self.fieldnames.get(uid) or list(messages[0].keys())
|
|
235
|
+
with open(filepath, "a", newline="", encoding="utf-8") as f:
|
|
236
|
+
# extrasaction="ignore": drop keys not in the file's header (e.g. a
|
|
237
|
+
# sensor that reappeared with a new field) instead of raising.
|
|
238
|
+
# restval="": leave missing keys blank instead of raising.
|
|
239
|
+
writer = csv.DictWriter(
|
|
240
|
+
f,
|
|
241
|
+
fieldnames=headers,
|
|
242
|
+
extrasaction="ignore",
|
|
243
|
+
restval="")
|
|
244
|
+
if not file_info["headers_written"]:
|
|
245
|
+
writer.writeheader()
|
|
246
|
+
file_info["headers_written"] = True
|
|
247
|
+
writer.writerows(messages)
|
|
248
|
+
|
|
249
|
+
def _write_json_batch(self, file_info: Dict,
|
|
250
|
+
messages: List[Dict[str, Any]]):
|
|
251
|
+
filepath = file_info["filepath"]
|
|
252
|
+
with open(filepath, "a", encoding="utf-8") as f:
|
|
253
|
+
for message in messages:
|
|
254
|
+
f.write(json.dumps(message) + "\n")
|
|
255
|
+
|
|
256
|
+
def flush_all(self):
|
|
257
|
+
"""Flush all device buffers to disk."""
|
|
258
|
+
for uid in list(self.buffers.keys()):
|
|
259
|
+
lock = self.buffer_locks.get(uid)
|
|
260
|
+
if lock:
|
|
261
|
+
with lock:
|
|
262
|
+
self._flush_device_buffer(uid)
|
|
263
|
+
else:
|
|
264
|
+
self._flush_device_buffer(uid)
|
|
265
|
+
|
|
266
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
267
|
+
return {
|
|
268
|
+
"active_devices": len(self.active_files),
|
|
269
|
+
"total_buffered": sum(len(b) for b in self.buffers.values()),
|
|
270
|
+
"buffer_sizes": {uid: len(b) for uid, b in self.buffers.items()},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class BatchingLogger:
|
|
275
|
+
"""High-level batching logger combining MessageBatcher and
|
|
276
|
+
CSVBatchWriter."""
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self,
|
|
280
|
+
output_dir: str,
|
|
281
|
+
config: BatchConfig = None,
|
|
282
|
+
format: str = "csv"):
|
|
283
|
+
self.config = config or BatchConfig()
|
|
284
|
+
self.format = format
|
|
285
|
+
self.logger = logging.getLogger(__name__)
|
|
286
|
+
|
|
287
|
+
self.writer = CSVBatchWriter(
|
|
288
|
+
output_dir, self.config.batch_size, self.format)
|
|
289
|
+
self.batcher = MessageBatcher(self.config)
|
|
290
|
+
self.batcher.set_flush_callback(self.writer.write_batch)
|
|
291
|
+
self.batcher.set_metrics_callback(self._on_metrics_update)
|
|
292
|
+
self.metrics_history: List[BatchMetrics] = []
|
|
293
|
+
|
|
294
|
+
def add_message(self, message: Dict[str, Any]) -> bool:
|
|
295
|
+
return self.batcher.add_message(message)
|
|
296
|
+
|
|
297
|
+
def add_messages(self, messages: List[Dict[str, Any]]) -> int:
|
|
298
|
+
return sum(1 for m in messages if self.add_message(m))
|
|
299
|
+
|
|
300
|
+
def flush(self) -> bool:
|
|
301
|
+
"""Flush all buffered messages to disk."""
|
|
302
|
+
# flushes batcher buffer -> writer.write_batch()
|
|
303
|
+
self.batcher.flush()
|
|
304
|
+
self.writer.flush_all() # flushes writer device buffers -> disk
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
def set_flush_callback(self, callback: Callable):
|
|
308
|
+
"""Retained for API compatibility — callback is no longer needed."""
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
def get_metrics(self) -> BatchMetrics:
|
|
312
|
+
return self.batcher.get_metrics()
|
|
313
|
+
|
|
314
|
+
def _on_metrics_update(self, metrics: BatchMetrics):
|
|
315
|
+
self.metrics_history.append(metrics)
|
|
316
|
+
if len(self.metrics_history) > 1000:
|
|
317
|
+
self.metrics_history.pop(0)
|
|
318
|
+
|
|
319
|
+
def get_performance_summary(self) -> Dict[str, Any]:
|
|
320
|
+
metrics = self.get_metrics()
|
|
321
|
+
return {
|
|
322
|
+
"buffer_utilization": f"{metrics.buffer_utilization:.1%}",
|
|
323
|
+
"total_messages": metrics.total_messages,
|
|
324
|
+
"total_batches": metrics.total_batches,
|
|
325
|
+
"avg_flush_time_ms": metrics.avg_flush_time * 1000,
|
|
326
|
+
"writer_stats": self.writer.get_stats(),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
def shutdown(self):
|
|
330
|
+
"""Shutdown the batching logger, flushing all remaining data."""
|
|
331
|
+
self.batcher.shutdown()
|
|
332
|
+
self.writer.flush_all()
|
|
333
|
+
self.logger.info("Batching logger shutdown complete")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# Convenience factories
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
def create_csv_batcher(
|
|
341
|
+
output_dir: str, batch_size: int = 100, flush_interval: float = 30.0
|
|
342
|
+
) -> BatchingLogger:
|
|
343
|
+
config = BatchConfig(batch_size=batch_size, flush_interval=flush_interval)
|
|
344
|
+
return BatchingLogger(output_dir, config, format="csv")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def create_json_batcher(
|
|
348
|
+
output_dir: str, batch_size: int = 500, flush_interval: float = 60.0
|
|
349
|
+
) -> BatchingLogger:
|
|
350
|
+
config = BatchConfig(batch_size=batch_size, flush_interval=flush_interval)
|
|
351
|
+
return BatchingLogger(output_dir, config, format="json")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def create_high_frequency_batcher(
|
|
355
|
+
output_dir: str, batch_size: int = 10, flush_interval: float = 5.0
|
|
356
|
+
) -> BatchingLogger:
|
|
357
|
+
config = BatchConfig(
|
|
358
|
+
batch_size=batch_size,
|
|
359
|
+
flush_interval=flush_interval,
|
|
360
|
+
max_buffer_size=1000)
|
|
361
|
+
return BatchingLogger(output_dir, config, format="csv")
|