Commit d88285e4 authored by Waleed Akbar's avatar Waleed Akbar
Browse files

feat: Refactor EventDispatcher to use domain names and link topology names for...

feat: Refactor EventDispatcher to use domain names and link topology names for connection processing

- Update SyntheticSampler to generate samples in specified range.
parent 99379c22
Loading
Loading
Loading
Loading
+16 −13
Original line number Diff line number Diff line
@@ -668,14 +668,14 @@ class EventDispatcher(BaseEventDispatcher):
            result = self._prepare_connection_processing(connection_uuid)
            if result is None:
                return False
            (topology_name, processed_links) = result
            (domain_name, processed_links) = result

            # Update telemetry for each link involved in this connection
            for link_uuid, link_name in processed_links:
            for link_uuid, link_name, link_topology_name in processed_links:
                # Count active connections on this link
                active_conn_count = self._count_active_connections(link_uuid, topology_name)
                active_conn_count = self._count_active_connections(link_uuid, domain_name)
                LOGGER.info('Connection {:s} uses allowed link: {:s} (uuid: {:s})'.format(connection_uuid, link_name, link_uuid))
                worker_name = '{:s}:{:s}'.format(topology_name, link_name)
                worker_name = '{:s}:{:s}'.format(link_topology_name, link_name)

                # Worker should already exist from _dispatch_link_set (link creation event)
                if not self._telemetry_pool.has_worker(WorkerTypeEnum.SYNTHESIZER, worker_name):
@@ -684,7 +684,7 @@ class EventDispatcher(BaseEventDispatcher):
                    # Create worker with same parameters as in _dispatch_link_set
                    resources = Resources()
                    resources.links.append(ResourceLink(
                        domain_name = topology_name,
                        domain_name = link_topology_name,
                        link_name   = link_name,
                        metrics_sampler = SyntheticSampler.create_random(
                            base_bw_range      = (5.0, 25.0),
@@ -748,7 +748,10 @@ class EventDispatcher(BaseEventDispatcher):
        for link_uuid in link_uuids:
            link = self._object_cache.get(CachedEntities.LINK, link_uuid)
            if link.name in allowed_link_names:
                processed_links.append((link_uuid, link.name))
                # Get the link's topology for worker naming
                link_topology_uuid, _ = get_link_endpoint(link)
                link_topology = self._object_cache.get(CachedEntities.TOPOLOGY, link_topology_uuid)
                processed_links.append((link_uuid, link.name, link_topology.name))

        if not processed_links:
            LOGGER.debug('Connection {:s} has no allowed links for domain {:s}'.format(
@@ -758,7 +761,7 @@ class EventDispatcher(BaseEventDispatcher):
        # Cache the connection-to-links mapping for later retrieval (e.g., during REMOVE events)
        mapping = {
            'domain': domain_name,
            'links': {link_uuid: {'name': link_name} for link_uuid, link_name in processed_links}
            'links': {link_uuid: {'name': link_name, 'topology': link_topo_name} for link_uuid, link_name, link_topo_name in processed_links}
        }
        self._object_cache.set(CachedEntities.CONNECTION, mapping, connection_uuid)
        LOGGER.debug('Cached connection {:s} mapping with {:d} links for domain {:s}'.format(
@@ -829,27 +832,27 @@ class EventDispatcher(BaseEventDispatcher):
                raise Exception(MSG.format(connection_uuid))
            
            # Extract domain and links from cached mapping
            topology_name   = mapping['domain']
            domain_name     = mapping['domain']
            link_uuids_dict = mapping['links']
            processed_links = [(link_uuid, link_data['name']) for link_uuid, link_data in link_uuids_dict.items()]
            processed_links = [(link_uuid, link_data['name'], link_data['topology']) for link_uuid, link_data in link_uuids_dict.items()]
            
            LOGGER.info('Retrieved cached mapping for connection {:s}: domain={:s}, links={:d}'.format(
                connection_uuid, topology_name, len(processed_links)))
                connection_uuid, domain_name, len(processed_links)))

            # Delete the connection from cache first (we already have the mapping)
            self._object_cache.delete(CachedEntities.CONNECTION, connection_uuid)
            LOGGER.debug('Deleted cached mapping for connection {:s}'.format(connection_uuid))

            # Process each link: count remaining connections and stop/update worker accordingly
            for link_uuid, link_name in processed_links:
                worker_name = '{:s}:{:s}'.format(topology_name, link_name)
            for link_uuid, link_name, link_topology_name in processed_links:
                worker_name = '{:s}:{:s}'.format(link_topology_name, link_name)

                if not self._telemetry_pool.has_worker(WorkerTypeEnum.SYNTHESIZER, worker_name):
                    LOGGER.warning('Worker not found for link {:s}, skipping telemetry update for connection removal'.format(link_name))
                    continue
                
                # Count remaining connections on this link (now excluding the deleted one)
                remaining_conn_count = self._count_active_connections(link_uuid, topology_name)
                remaining_conn_count = self._count_active_connections(link_uuid, domain_name)
                
                if remaining_conn_count == 0:
                    # No other connections use this link, stop the worker
+59 −86
Original line number Diff line number Diff line
@@ -13,115 +13,88 @@
# limitations under the License.


import math, random, sys, threading
import random, threading
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, Optional, Tuple
from .Sample import Sample

# Congestion curve types
CURVE_LINEAR      = 'linear'       # x - steady increase
CURVE_EXPONENTIAL = 'exponential'  # exp(x)-1 - slow start, rapid end
CURVE_LOGARITHMIC = 'logarithmic'  # log(1+x) - fast start, plateau

MAX_CONNECTIONS = 5

# LINEAR curve target ranges: 0 conns (5-15%), 5+ conns (90-99%)
LINEAR_MIN_BASE = 5.0   # Minimum BW at 0 connections
LINEAR_MIN_FULL = 90.0  # Minimum BW at 5+ connections
LINEAR_MAX_BASE = 15.0  # Maximum BW at 0 connections
LINEAR_MAX_FULL = 99.0  # Maximum BW at 5+ connections


@dataclass
class SyntheticSampler:
    base_bw          : float = field(default = 10.0)   # Base bandwidth utilization %
    base_latency     : float = field(default = 1.0)    # Base latency in ms
    sensitivity      : float = field(default = 0.5)    # Congestion sensitivity (0.0-1.0)
    curve_type       : str   = field(default = CURVE_LINEAR)
    connection_count : int   = field(default = 0)      # Current connection count for load simulation
    """Simple sampler with temporal continuity - next values stay close to previous values.
    
    Bandwidth ranges based on connection count:
      0 conns: avg=5%,  range 0-10%
      1 conn:  avg=25%, range 10-40%
      2 conns: avg=45%, range 30-60%
      3 conns: avg=65%, range 50-80%
      4+ conns: avg=85%, range 70-90%
    
    Latency scales proportionally with bandwidth (0% BW → 1ms, 100% BW → 20ms).
    Values vary by ±5% between consecutive samples for realistic jitter.
    """
    connection_count : int             = field(default = 0)     # Current connection count
    link_capacity    : float           = field(default = 100.0) # Link capacity in Gbps
    bw_min_value     : float = field(default = 0.0)
    bw_max_value     : float = field(default = 100.0)
    lat_min_value    : float = field(default = 0.1)
    lat_max_value    : float = field(default = 20.0)
    prev_bw          : Optional[float] = field(default = None)  # Previous BW percentage
    prev_latency     : Optional[float] = field(default = None)  # Previous latency (ms)
    
    # Connection count to (avg, min, max) percentage mapping
    BW_RANGES = {
        0: (3,  0,  10),
        1: (25, 10, 40),
        2: (45, 30, 60),
        3: (65, 50, 80),
        4: (85, 70, 90),
    }

    @classmethod
    def create_random(
        cls, base_bw_range : Tuple[float, float] = (0.0, 1.0),
        base_latency_range : Tuple[float, float] = (1.0, 3.0),
        sensitivity_range : Tuple[float, float] = (0.0, 1.0),
        curve_type : Optional[str] = CURVE_LINEAR,
        cls,
        connection_count   : int    = 0,
        link_capacity      : float  = 100.0
    ) -> 'SyntheticSampler':
        """Create a random sampler with congestion curve parameters.
        
        For LINEAR: base_bw and sensitivity interpolate within target ranges per connection count.
        For EXPONENTIAL/LOGARITHMIC: uses traditional curve formulas.
        """
        base_bw = random.uniform(base_bw_range[0], base_bw_range[1])
        base_latency = random.uniform(base_latency_range[0], base_latency_range[1])
        sensitivity = random.uniform(sensitivity_range[0], sensitivity_range[1])
        
        return cls(base_bw, base_latency, sensitivity, curve_type, connection_count, link_capacity)

    def _compute_congestion_factor(self, load_ratio: float) -> float:
        """Compute congestion factor based on curve type and load ratio (0-1)."""
        if self.curve_type == CURVE_LINEAR:
            return load_ratio
        elif self.curve_type == CURVE_EXPONENTIAL:
            # Exponential: slow start, rapid increase at high load
            return (math.exp(load_ratio * 2) - 1) / (math.e ** 2 - 1)
        elif self.curve_type == CURVE_LOGARITHMIC:
            # Logarithmic: fast initial increase, then plateau
            return math.log1p(load_ratio * 2.7) / math.log1p(2.7)
        else:
            return load_ratio  # Default to linear
        """Factory method for compatibility (ignores unused parameters)."""
        return cls(connection_count=connection_count, link_capacity=link_capacity)

    def get_sample(self) -> Tuple[Sample, Sample]:
        """Generate both bandwidth and latency samples using congestion curves.
        
        LINEAR curve uses simple linear interpolation between min/max lines.
        EXPONENTIAL/LOGARITHMIC use formula-based congestion factors.
        """Generate bandwidth and latency samples with temporal continuity.
        
        Returns:
            Tuple of (bandwidth_sample, latency_sample)
        """
        timestamp = datetime.now().timestamp()
        
        if self.curve_type == CURVE_LINEAR:
            # Simple linear interpolation
            # Connection ratio: 0 (no load) to 1 (max load)
            conn_ratio = min(self.connection_count / MAX_CONNECTIONS, 1.0) if MAX_CONNECTIONS > 0 else 0.0
        # Determine range based on connection count (cap at 4+)
        conn_key = min(self.connection_count, 4)
        avg, min_bw, max_bw = self.BW_RANGES[conn_key]
        
            # Minimum line: 5% at 0 conns → 90% at 5 conns
            min_bw = LINEAR_MIN_BASE + conn_ratio * (LINEAR_MIN_FULL - LINEAR_MIN_BASE)
        # Generate bandwidth percentage
        if self.prev_bw is None:
            # First sample: start at average for this connection count
            bw_utilization = avg
        else:
            # Add ±2% noise to previous value for temporal continuity
            noise_factor = random.uniform(-0.02, 0.02)
            bw_utilization = self.prev_bw * (1.0 + noise_factor)
        
            # Maximum line: 15% at 0 conns → 99% at 5 conns
            max_bw = LINEAR_MAX_BASE + conn_ratio * (LINEAR_MAX_FULL - LINEAR_MAX_BASE)
        # Clamp to current range (handles "jump" when connection count changes)
        bw_utilization = max(min_bw, min(max_bw, bw_utilization))
        self.prev_bw = bw_utilization
        
            # Interpolate between min and max using base_bw and sensitivity
            interpolation_factor = (self.base_bw + self.sensitivity) / 2.0
            bw_utilization = min_bw + (max_bw - min_bw) * interpolation_factor
        # Latency scales proportionally with bandwidth (1ms at 0%, 20ms at 100%)
        target_latency = 1.0 + (bw_utilization / 100.0) * 19.0
        
            # Latency scales proportionally (10x increase from base at full load)
            bw_normalized = (bw_utilization - LINEAR_MIN_BASE) / (LINEAR_MAX_FULL - LINEAR_MIN_BASE)
            latency = self.base_latency * (1.0 + bw_normalized * 9.0)
        if self.prev_latency is None:
            latency = target_latency
        else:
            # Use formula-based approach for EXPONENTIAL/LOGARITHMIC
            load_ratio = self.connection_count / MAX_CONNECTIONS if MAX_CONNECTIONS > 0 else 0.0
            congestion_factor = self._compute_congestion_factor(load_ratio)
            
            bw_utilization = self.base_bw + (congestion_factor * self.sensitivity * 70.0)
            latency = self.base_latency * (1.0 + congestion_factor * self.sensitivity * 9.0)
        
        # Add uniform noise (5%)
        bw_noise = random.uniform(-0.05, 0.05) * bw_utilization
        lat_noise = random.uniform(-0.05, 0.05) * latency
            # Add ±2% noise to previous latency
            noise_factor = random.uniform(-0.02, 0.02)
            latency = self.prev_latency * (1.0 + noise_factor)
        
        bw_utilization = max(self.bw_min_value, min(self.bw_max_value, bw_utilization + bw_noise))
        latency = max(self.lat_min_value, min(self.lat_max_value, latency + lat_noise))
        # Clamp latency to reasonable range
        latency = max(0.5, min(25.0, latency))
        self.prev_latency = latency
        
        # Convert percentage to actual utilization (Gbps)
        actual_bw_utilization = (bw_utilization / 100.0) * self.link_capacity