def decide_next_node(self, flow: Flow): """ Check for conflicting events to schedule per-flow decisions from external algorithms If conflicting event exists, yield a backoff time before triggering the event Return `External` to indicate that a decision from an external algorithm is required for the flow """ if flow.ttl <= 0: return None # If flow is done processing and at egress, don't request new decision and just let flow depart if flow.forward_to_eg and flow.current_node_id == flow.egress_node_id: return flow.current_node_id events_list = self.env._queue events_now = [event for event in events_list if event[0] == self.env.now] for event in events_now: event_object = event[-1] if isinstance(event_object, simpy.events.Event) and event_object.value is not None: # There is a scheduling conflict, wait a backoff time (between 0 and 1) and restart backoff_time = random.random() / 10 yield self.env.timeout(backoff_time) if not flow.forward_to_eg: sf = self.params.sfc_list[flow.sfc][flow.current_position] else: # a virtual EG sf for flows on their way to egress sf = "EG" flow.current_sf = sf self.params.metrics.add_requesting_flow(flow) # Trigger the event to register in Simpy self.params.flow_trigger.succeed(value=flow) # Reset it immediately self.params.flow_trigger = self.env.event() # Check flow TTL and drop if zero or less return "External"
def generate_flow(self, flow_id, node_id) -> Tuple[float, Flow]: """ Generate a flow for a given node_id """ inter_arr_time, flow_dr, flow_size = self.params.get_next_flow_data( node_id) # Assign a random SFC to the flow flow_sfc = np.random.choice( [sfc for sfc in self.params.sfc_list.keys()]) # Get the flow's creation time (current environment time) creation_time = self.env.now # Set the egress node for the flow if some are specified in the network file flow_egress_node = None if self.params.eg_nodes: flow_egress_node = random.choice(self.params.eg_nodes) # Generate flow based on given params ttl = random.choice(self.params.ttl_choices) # Generate flow based on given params flow = Flow(str(flow_id), flow_sfc, flow_dr, flow_size, creation_time, current_node_id=node_id, egress_node_id=flow_egress_node, ttl=ttl) # Update metrics for the generated flow self.params.metrics.generated_flow(flow, node_id) return inter_arr_time, flow
def request_resources(self, flow: Flow, node_id: str, sf: str) -> bool: """ Request resources from the node Returns True if resources were successfully given to the flow """ # Calculate the demanded capacity when the flow is processed at this node demanded_total_capacity = self.get_demanded_cap(flow.dr, node_id, sf) # Get node capacities node_cap = self.params.network.nodes[node_id]["cap"] node_remaining_cap = self.params.network.nodes[node_id][ "remaining_cap"] assert node_remaining_cap >= 0, "Remaining node capacity cannot be less than 0 (zero)!" if demanded_total_capacity <= node_cap: self.params.logger.info( "Flow {} started processing at sf {} at node {}. Time: {}". format(flow.flow_id, sf, node_id, self.env.now)) # Update processing level of flow flow.processing_index += 1 # Metrics: Add active flow to the SF once the flow has begun processing. self.params.metrics.add_active_flow(flow, node_id, sf) # Add load to sf self.params.network.nodes[node_id]['available_sf'][sf][ 'load'] += flow.dr # Set remaining node capacity self.params.network.nodes[node_id][ 'remaining_cap'] = node_cap - demanded_total_capacity # Set max node usage self.params.metrics.calc_max_node_usage(node_id, demanded_total_capacity) # Just for the sake of keeping lines small, the node_remaining_cap is updated again. node_remaining_cap = self.params.network.nodes[node_id][ "remaining_cap"] # Check if startup is done startup_time = self.params.network.nodes[node_id]['available_sf'][ sf]['startup_time'] startup_delay = self.params.sf_list[sf]["startup_delay"] startup_done = True if (startup_time + startup_delay) <= self.env.now else False if not startup_done: # Startup is not done: wait the remaining startup time startup_time_remaining = (startup_time + startup_delay) - self.env.now # Check if startup delay will cause flow to be dropped if flow.ttl - startup_time_remaining <= 0: flow.ttl = 0 return False flow.end2end_delay += startup_time_remaining flow.ttl -= startup_time_remaining yield self.env.timeout(startup_time_remaining) return True else: self.params.logger.info( f"Not enough capacity for flow {flow.flow_id} at node {flow.current_node_id}. Dropping flow." ) return False
def get_processing_delay(self, flow: Flow, sf: str) -> float: """ Generate a random processing delay based on mean and stdev from sf file """ vnf_delay_mean = self.params.sf_list[sf]["processing_delay_mean"] vnf_delay_stdev = self.params.sf_list[sf]["processing_delay_stdev"] processing_delay = np.absolute( np.random.normal(vnf_delay_mean, vnf_delay_stdev)) if flow.ttl - processing_delay <= 0: flow.ttl = 0 return False self.params.metrics.add_processing_delay(processing_delay) flow.end2end_delay += processing_delay flow.ttl -= processing_delay return processing_delay
def generate_flow(self, node_id): """ Generate flows at the ingress nodes. """ while self.params.inter_arr_mean[node_id] is not None: self.total_flow_count += 1 if self.params.flow_list_idx is None: # Poisson arrival -> exponential distributed inter-arrival time inter_arr_time = random.expovariate(lambd=1.0/self.params.inter_arr_mean[node_id]) # set normally distributed flow data rate flow_dr = np.random.normal(self.params.flow_dr_mean, self.params.flow_dr_stdev) if self.params.deterministic_size: flow_size = self.params.flow_size_shape else: # heavy-tail flow size flow_size = np.random.pareto(self.params.flow_size_shape) + 1 # Skip flows with negative flow_dr or flow_size values if flow_dr <= 0.00 or flow_size <= 0.00: continue # use generated list of flow arrivals else: inter_arr_time, flow_dr, flow_size = self.params.get_next_flow_data(node_id) # Assign a random SFC to the flow flow_sfc = np.random.choice([sfc for sfc in self.params.sfc_list.keys()]) # Get the flow's creation time (current environment time) creation_time = self.env.now # Set the egress node for the flow if some are specified in the network file flow_egress_node = None if self.params.eg_nodes: flow_egress_node = random.choice(self.params.eg_nodes) # Generate flow based on given params flow = Flow(str(self.total_flow_count), flow_sfc, flow_dr, flow_size, creation_time, current_node_id=node_id, egress_node_id=flow_egress_node) # Update metrics for the generated flow self.params.metrics.generated_flow(flow, node_id) # Generate flows and schedule them at ingress node self.env.process(self.init_flow(flow)) yield self.env.timeout(inter_arr_time)
def generate_flow(self, node_id): """ Generate flows at the ingress nodes. """ while True: self.total_flow_count += 1 # set normally distributed flow data rate flow_dr = np.random.normal(self.params.flow_dr_mean, self.params.flow_dr_stdev) # set deterministic or random flow arrival times and flow sizes according to config if self.params.deterministic_arrival: inter_arr_time = self.params.inter_arr_mean else: # Poisson arrival -> exponential distributed inter-arrival time inter_arr_time = random.expovariate(lambd=1.0/self.params.inter_arr_mean) if self.params.deterministic_size: flow_size = self.params.flow_size_shape else: # heavy-tail flow size flow_size = np.random.pareto(self.params.flow_size_shape) + 1 # Skip flows with negative flow_dr or flow_size values if flow_dr <= 0.00 or flow_size <= 0.00: continue # Assign a random SFC to the flow flow_sfc = np.random.choice([sfc for sfc in self.params.sfc_list.keys()]) # Get the flow's creation time (current environment time) creation_time = self.env.now # Generate flow based on given params flow = Flow(str(self.total_flow_count), flow_sfc, flow_dr, flow_size, creation_time, current_node_id=node_id) # Update metrics for the generated flow metrics.generated_flow(flow, node_id) # Generate flows and schedule them at ingress node self.env.process(self.init_flow(flow)) yield self.env.timeout(inter_arr_time)
def finish_processing(self, flow: Flow, node_id: str, sf: str) -> bool: """ Simpy process to cleanup used resources after a flow has finished processing """ flow.current_position += 1 if flow.current_position == len(self.params.sfc_list[flow.sfc]): flow.forward_to_eg = True # Wait flow duration for flow to fully process yield self.env.timeout(flow.duration) # Remove the active flow from the node self.params.metrics.remove_active_flow(flow, node_id, sf) # Remove flow's load from sf self.params.network.nodes[node_id]['available_sf'][sf][ 'load'] -= flow.dr assert self.params.network.nodes[node_id]['available_sf'][sf]['load'] >= 0, \ 'SF load cannot be less than 0!' # Remove SF gracefully from node if no load exists and SF removed from placement if (self.params.network.nodes[node_id]['available_sf'][sf]['load'] == 0) and (sf not in self.params.sf_placement[node_id]): del self.params.network.nodes[node_id]['available_sf'][sf] # Recalculate used node cap before updating node rem. cap because of how simpy schedules processes node_cap = self.params.network.nodes[node_id]["cap"] used_total_capacity = 0.0 for sf_i, sf_data in self.params.network.nodes[node_id][ 'available_sf'].items(): if not sf_i == "EG": used_total_capacity += self.params.sf_list[sf_i][ 'resource_function'](sf_data['load']) # Set remaining node capacity self.params.network.nodes[node_id][ 'remaining_cap'] = node_cap - used_total_capacity node_remaining_cap = self.params.network.nodes[node_id][ "remaining_cap"] # We assert that remaining capacity must at all times be less than the node capacity so that # nodes dont put back more capacity than the node's capacity. assert node_remaining_cap <= node_cap, "Node remaining capacity cannot be more than node capacity!"
def generate_flow(self, flow_id, node_id) -> Tuple[float, Flow]: """ Generate a flow for a given node_id """ if self.params.deterministic_arrival: inter_arr_time = self.params.inter_arr_mean[node_id] else: # Poisson arrival -> exponential distributed inter-arrival time inter_arr_time = random.expovariate( lambd=1.0 / self.params.inter_arr_mean[node_id]) flow_dr, flow_size = self.get_flow_dr_size() # if flow_dr <= 0.00 or flow_size <= 0.00: # continue # Assign a random SFC to the flow flow_sfc = np.random.choice( [sfc for sfc in self.params.sfc_list.keys()]) # Get the flow's creation time (current environment time) creation_time = self.env.now # Set the egress node for the flow if some are specified in the network file flow_egress_node = None if self.params.eg_nodes: flow_egress_node = random.choice(self.params.eg_nodes) # Generate flow based on given params ttl = random.choice(self.params.ttl_choices) # Generate flow based on given params flow = Flow(str(flow_id), flow_sfc, flow_dr, flow_size, creation_time, current_node_id=node_id, egress_node_id=flow_egress_node, ttl=ttl) # Update metrics for the generated flow self.params.metrics.generated_flow(flow, node_id) return inter_arr_time, flow
def decide_next_node(self, flow: Flow): """ Load balance the flows according to the scheduling tables """ # Blank timeout to convert it to a simpy process yield self.env.timeout(0) # Check flow TTL and drop if zero or less if flow.ttl <= 0: return None # If flow is to be forwarded to egress, always return egress node as "next node" if flow.forward_to_eg: if flow.egress_node_id is None: # If flow has no egress node: set current node_id to egress node_id flow.egress_node_id = flow.current_node_id return flow.egress_node_id sf = self.params.sfc_list[flow.sfc][flow.current_position] flow.current_sf = sf self.params.metrics.add_requesting_flow(flow) schedule = self.params.schedule # Check if scheduling rule exists if (flow.current_node_id in schedule) and flow.sfc in schedule[flow.current_node_id]: local_schedule = schedule[flow.current_node_id][flow.sfc][sf] dest_nodes = [sch_sf for sch_sf in local_schedule.keys()] dest_prob = [prob for name, prob in local_schedule.items()] try: # select next node based on weighted RR according to the scheduling weights/probabilities # get current flow counts per possible destination node flow_counts = self.params.metrics.metrics['run_flow_counts'][ flow.current_node_id][flow.sfc][sf] flow_sum = sum(flow_counts.values()) # calculate the current ratios of flows sent to the different destination nodes if flow_sum > 0: dest_ratios = [ flow_counts[v] / flow_sum for v in dest_nodes ] else: dest_ratios = [0 for v in dest_nodes] # calculate the difference from the scheduling weight # for nodes with 0 probability/weight, set the diff to be negative so they are not selected # otherwise all diffs may be 0 if ratio = probability and a node with probability 0 could be selected assert len(dest_nodes) == len(dest_prob) == len(dest_ratios) ratio_diffs = [ dest_prob[i] - dest_ratios[i] if dest_prob[i] > 0 else -1 for i in range(len(dest_nodes)) ] # select the node that farthest away from its weight, ie, has the highest diff max_idx = np.argmax(ratio_diffs) next_node = dest_nodes[max_idx] # increase counter for selected node self.params.metrics.metrics['run_flow_counts'][ flow.current_node_id][flow.sfc][sf][next_node] += 1 return next_node except Exception as ex: # Scheduling rule does not exist: drop flow self.params.loogger.warning( f'Flow {flow.flow_id}: Scheduling rule at node {flow.current_node_id} not correct' f'Dropping flow!') self.params.logger.warning(ex) return None else: # Scheduling rule does not exist: drop flow self.params.logger.warning( f'Flow {flow.flow_id}: Scheduling rule not found at {flow.current_node_id}. Dropping flow!' ) return None