Skip to content

Commit e56cc2f

Browse files
committed
feat(core): add background entropy collector thread
- Daemon thread for continuous entropy collection - Configurable interval between collections - Uses all available harvesters automatically
1 parent 3cfc49d commit e56cc2f

1 file changed

Lines changed: 285 additions & 0 deletions

File tree

src/trueentropy/collector.py

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# =============================================================================
2+
# TrueEntropy - Background Collector Module
3+
# =============================================================================
4+
#
5+
# This module provides the background entropy collection thread. It runs
6+
# silently in the background, periodically harvesting entropy from all
7+
# available sources and feeding it into the pool.
8+
#
9+
# Benefits:
10+
# - Pool stays full even during heavy random number generation
11+
# - Entropy is collected from diverse sources over time
12+
# - Users don't need to manually manage entropy collection
13+
#
14+
# Usage:
15+
# from trueentropy import start_collector, stop_collector
16+
#
17+
# start_collector(interval=2.0) # Collect every 2 seconds
18+
# # ... application runs ...
19+
# stop_collector() # Clean shutdown
20+
#
21+
# =============================================================================
22+
23+
"""
24+
Background entropy collector thread.
25+
26+
Provides automatic, continuous entropy collection from all available
27+
sources, keeping the entropy pool full.
28+
"""
29+
30+
from __future__ import annotations
31+
32+
import logging
33+
import threading
34+
import time
35+
from typing import List, Optional
36+
37+
from trueentropy.pool import EntropyPool
38+
from trueentropy.harvesters.base import BaseHarvester, HarvestResult
39+
from trueentropy.harvesters.timing import TimingHarvester
40+
from trueentropy.harvesters.network import NetworkHarvester
41+
from trueentropy.harvesters.system import SystemHarvester
42+
from trueentropy.harvesters.external import ExternalHarvester
43+
44+
45+
# -----------------------------------------------------------------------------
46+
# Module-level Logger
47+
# -----------------------------------------------------------------------------
48+
49+
logger = logging.getLogger("trueentropy.collector")
50+
51+
52+
# -----------------------------------------------------------------------------
53+
# Global State
54+
# -----------------------------------------------------------------------------
55+
56+
# The collector thread (None if not running)
57+
_collector_thread: Optional[threading.Thread] = None
58+
59+
# Event to signal the collector to stop
60+
_stop_event: Optional[threading.Event] = None
61+
62+
63+
# -----------------------------------------------------------------------------
64+
# Public Functions
65+
# -----------------------------------------------------------------------------
66+
67+
def start_background_collector(
68+
pool: EntropyPool,
69+
interval: float = 1.0,
70+
enable_network: bool = True,
71+
enable_external: bool = True
72+
) -> None:
73+
"""
74+
Start the background entropy collector thread.
75+
76+
The collector runs in a daemon thread, periodically harvesting
77+
entropy from all available sources and feeding it into the pool.
78+
79+
Args:
80+
pool: The EntropyPool to feed entropy into
81+
interval: Seconds between collection cycles (default: 1.0)
82+
enable_network: Whether to enable network harvester (default: True)
83+
enable_external: Whether to enable external API harvester (default: True)
84+
85+
Note:
86+
- Only one collector can run at a time
87+
- The collector is a daemon thread (exits when main program exits)
88+
- Call stop_background_collector() for clean shutdown
89+
90+
Example:
91+
>>> from trueentropy import get_pool
92+
>>> from trueentropy.collector import start_background_collector
93+
>>> pool = get_pool()
94+
>>> start_background_collector(pool, interval=2.0)
95+
"""
96+
global _collector_thread, _stop_event
97+
98+
# Check if already running
99+
if _collector_thread is not None and _collector_thread.is_alive():
100+
logger.warning("Background collector is already running")
101+
return
102+
103+
# Create stop event
104+
_stop_event = threading.Event()
105+
106+
# Create and configure harvesters
107+
harvesters: List[BaseHarvester] = [
108+
TimingHarvester(),
109+
SystemHarvester(),
110+
]
111+
112+
if enable_network:
113+
harvesters.append(NetworkHarvester())
114+
115+
if enable_external:
116+
harvesters.append(ExternalHarvester())
117+
118+
# Create collector thread
119+
_collector_thread = threading.Thread(
120+
target=_collector_loop,
121+
args=(pool, harvesters, interval, _stop_event),
122+
name="TrueEntropy-Collector",
123+
daemon=True # Allows clean exit when main program ends
124+
)
125+
126+
# Start the thread
127+
_collector_thread.start()
128+
logger.info(
129+
f"Background collector started with {len(harvesters)} harvesters, "
130+
f"interval={interval}s"
131+
)
132+
133+
134+
def stop_background_collector(timeout: float = 5.0) -> bool:
135+
"""
136+
Stop the background entropy collector thread.
137+
138+
Signals the collector to stop and waits for it to finish.
139+
140+
Args:
141+
timeout: Maximum seconds to wait for thread to stop
142+
143+
Returns:
144+
True if the collector stopped cleanly, False if it timed out
145+
146+
Example:
147+
>>> from trueentropy.collector import stop_background_collector
148+
>>> success = stop_background_collector()
149+
>>> print("Stopped cleanly" if success else "Timeout")
150+
"""
151+
global _collector_thread, _stop_event
152+
153+
if _collector_thread is None or not _collector_thread.is_alive():
154+
logger.debug("Background collector is not running")
155+
return True
156+
157+
if _stop_event is None:
158+
logger.error("Stop event is None but thread is running")
159+
return False
160+
161+
# Signal the collector to stop
162+
_stop_event.set()
163+
logger.debug("Stop signal sent to collector")
164+
165+
# Wait for thread to finish
166+
_collector_thread.join(timeout=timeout)
167+
168+
if _collector_thread.is_alive():
169+
logger.warning(f"Collector did not stop within {timeout}s")
170+
return False
171+
172+
logger.info("Background collector stopped")
173+
174+
# Clean up
175+
_collector_thread = None
176+
_stop_event = None
177+
178+
return True
179+
180+
181+
def is_collector_running() -> bool:
182+
"""
183+
Check if the background collector is currently running.
184+
185+
Returns:
186+
True if the collector thread is alive
187+
"""
188+
return _collector_thread is not None and _collector_thread.is_alive()
189+
190+
191+
# -----------------------------------------------------------------------------
192+
# Private Functions
193+
# -----------------------------------------------------------------------------
194+
195+
def _collector_loop(
196+
pool: EntropyPool,
197+
harvesters: List[BaseHarvester],
198+
interval: float,
199+
stop_event: threading.Event
200+
) -> None:
201+
"""
202+
Main loop for the background collector thread.
203+
204+
This function runs in a separate thread, periodically calling
205+
each harvester and feeding the results into the pool.
206+
207+
Args:
208+
pool: The EntropyPool to feed entropy into
209+
harvesters: List of harvesters to use
210+
interval: Seconds between collection cycles
211+
stop_event: Event to signal when to stop
212+
"""
213+
logger.debug("Collector loop started")
214+
215+
while not stop_event.is_set():
216+
# Track timing for this cycle
217+
cycle_start = time.perf_counter()
218+
219+
# Collect from all harvesters
220+
total_bits = 0
221+
successful = 0
222+
223+
for harvester in harvesters:
224+
# Use safe_collect to handle any exceptions
225+
result = harvester.safe_collect()
226+
227+
if result.success:
228+
# Feed the collected entropy into the pool
229+
pool.feed(result.data, entropy_estimate=result.entropy_bits)
230+
total_bits += result.entropy_bits
231+
successful += 1
232+
logger.debug(
233+
f"Harvester '{result.source}' collected "
234+
f"{result.entropy_bits} bits"
235+
)
236+
else:
237+
logger.debug(
238+
f"Harvester '{result.source}' failed: {result.error}"
239+
)
240+
241+
# Log cycle summary
242+
cycle_time = time.perf_counter() - cycle_start
243+
logger.debug(
244+
f"Collection cycle complete: {successful}/{len(harvesters)} "
245+
f"harvesters, {total_bits} bits, {cycle_time:.3f}s"
246+
)
247+
248+
# Wait for the next cycle (or until stop is signaled)
249+
# We use wait() instead of sleep() so we can respond to stop quickly
250+
remaining_interval = max(0, interval - cycle_time)
251+
stop_event.wait(timeout=remaining_interval)
252+
253+
logger.debug("Collector loop exiting")
254+
255+
256+
def collect_once(pool: EntropyPool) -> int:
257+
"""
258+
Perform a single collection cycle.
259+
260+
This is useful for manually triggering collection without
261+
running the background thread.
262+
263+
Args:
264+
pool: The EntropyPool to feed entropy into
265+
266+
Returns:
267+
Total entropy bits collected
268+
"""
269+
harvesters: List[BaseHarvester] = [
270+
TimingHarvester(),
271+
SystemHarvester(),
272+
NetworkHarvester(),
273+
ExternalHarvester(),
274+
]
275+
276+
total_bits = 0
277+
278+
for harvester in harvesters:
279+
result = harvester.safe_collect()
280+
281+
if result.success:
282+
pool.feed(result.data, entropy_estimate=result.entropy_bits)
283+
total_bits += result.entropy_bits
284+
285+
return total_bits

0 commit comments

Comments
 (0)