Source code for onbrisca.models.bridge

# SPDX-FileCopyrightText: 2022 The Tor Project, Inc.
#
# SPDX-License-Identifier: BSD-3-Clause
import logging
import random

from asgiref.sync import sync_to_async
from django.db import models

from onbasca.base.models.bridge import BridgeBase, BridgeManagerBase
from onbasca.onbasca.models import Consensus
from onbrisca import config

logger = logging.getLogger(__name__)


[docs] class BridgeManager(BridgeManagerBase): """BridgeManager model for operations with all the bridges."""
[docs] def bridgelines(self) -> list: bridge_list = [ bridge.bridgeline for bridge in Bridge.objects.all() if bridge.bridgeline ] return bridge_list
[docs] def bridgelines_from_bridges(self, bridges) -> list: """Return a bridgelines list for the given set of bridges.""" # logger.debug("bridges %s", bridges) bridge_list = [ bridge.bridgeline for bridge in bridges if bridge.bridgeline ] logger.debug("bridge list %s", bridge_list) return bridge_list
[docs] def bridgelines_config(self) -> dict: bridge_dict = { "Bridge": self.bridgelines(), } return bridge_dict
[docs] def mu(self): return self.annotate( bridge_bw_mean=models.Avg("measurements__bandwidth") ).aggregate(models.Avg("bridge_bw_mean"))["bridge_bw_mean__avg"]
[docs] async def acount(self): return sync_to_async(self.count())
[docs] def ordered(self): logger.debug("Prioritizing to measure bridges without measurements.") # The bridges are ordered by default (`get_latest_by`) by # `_last_measured` ascending. Because here we are selecting the # bridges without any measurement, then they will be ordered by # `_obj_updated_at` descending, ie. the ones more recently updated # (or created) first. bridges_no_measurements = self.filter( measurements__isnull=True, ).distinct() bridges = bridges_no_measurements[: config.NUM_BRIDGES_LOOP] bridges_count = bridges.count() logger.debug("Adding %s bridges never measured yet.", bridges_count) if bridges_count < config.NUM_BRIDGES_LOOP: bridges_to_add = config.NUM_BRIDGES_LOOP - bridges_count logger.debug("Adding %s bridges already measured.", bridges_to_add) # Then select the bridges that have been already measured, the ones # with older measurements first. If 2 bridges were measured at the # same time, the one most recently updated (or created) is measured # first. bridges_measurements = self.filter( measurements__isnull=False ).distinct() bridges = list(bridges) bridges.extend(list(bridges_measurements[:bridges_to_add])) # Convert it to list when it's still a queryset, so that it's always # a list return list(bridges)
[docs] def muf(self): return ( self.annotate(bridge_bw_mean=models.Avg("measurements__bandwidth")) .filter(measurements__bandwidth__gte=models.F("bridge_bw_mean")) .aggregate(models.Avg("bridge_bw_mean"))["bridge_bw_mean__avg"] )
[docs] def delete_invalid(self): """ Delete the bridges in the database that have invalid bridgelines, so that they don't fail to be set to tor configuration. """ from onbasca.bridgeline import parse_bridge_line logger.debug("Deleting bridges with invalid bridgelines.") for bridge in self.all(): if not parse_bridge_line(bridge.bridgeline): logger.warning( "Deleting bridge with invalid bridgeline %s.", bridge.bridgeline, ) bridge.delete()
[docs] class Bridge(BridgeBase): """Bridge model that calculates and saves the stream ratio.""" class Meta: # Latest by ascending `_last_measured` and descending `_obj_updated_at` get_latest_by = ["_last_measured", "-_obj_updated_at"] objects = BridgeManager() _bw_mean = models.PositiveIntegerField(null=True, blank=True) _bw_filt = models.PositiveIntegerField(null=True, blank=True) _ratio_stream = models.FloatField(null=True, blank=True) _last_measured = models.DateTimeField(blank=True, null=True) _ratio_filt = models.FloatField(null=True, blank=True) _ratio = models.FloatField(null=True, blank=True)
[docs] async def asave(self, *args, **kwargs): await sync_to_async(super().save)(*args, **kwargs)
[docs] async def helper_path(self, consensus: Consensus) -> list: """ """ logger.debug("Creating path for bridge %s.", self) middle_candidates_fingerprints = ( await consensus.aget_fast_stable_uptime_non_exits_fingerprints() ) middle_fp = await sync_to_async(random.choice)( middle_candidates_fingerprints ) exit_candidates_fingerprints = ( await consensus.aget_fast_exits_min_bandwidth() ) exit_fp = await sync_to_async(random.choice)( exit_candidates_fingerprints ) path = [self.fingerprint, middle_fp, exit_fp] logger.debug("Created path: %s", path) return path
[docs] def set_bw_mean(self): self._bw_mean = ( self.measurements.aggregate(models.Avg("bandwidth"))[ "bandwidth__avg" ] or 0 ) # logger.debug("Bridge %s bw mean: %s", self, self._bw_mean) self.save() return self._bw_mean
[docs] def set_bw_filt(self): # This should be the same as: # self.measurements.annotate(bw_mean=models.Avg("bandwidth")) # .filter(bandwidth__gte=models.F("bw_mean")) # .aggregate(models.Avg("bandwidth"))["bandwidth__avg"] self._bw_filt = ( self.measurements.filter(bandwidth__gte=self._bw_mean).aggregate( models.Avg("bandwidth") )["bandwidth__avg"] or self._bw_mean ) # logger.debug("Bridge %s bw mean filtered: %s", self, self._bw_mean) self.save() return self._bw_filt
[docs] def set_ratio_stream(self, mu): if not mu: # logger.warning("Unexpected mu %s", mu) # If there aren't measurements (mu is None), set ratio to 0 too. self._ratio_stream = 0 else: self._ratio_stream = self._bw_mean / mu # logger.debug("Bridge %s stream ratio: %s", self, self._ratio_stream) self.save() return self._ratio_stream
[docs] def set_ratio_filt(self, muf): if not muf: # logger.warning("Unexpected muf %s", muf) self._ratio_filt = 0 else: self._ratio_filt = self._bw_filt / muf # logger.debug("Bridge filtered stream ratio: %s", self._ratio_filt) self.save() return self._ratio_filt
[docs] def set_ratio(self): # if self._ratio_stream > self._ratio_filt: # logger.info("Stream ratio greater than filtered.") self._ratio = max(self._ratio_stream, self._ratio_filt) logger.debug("Bridge ratio: %s", self._ratio) self.save() return self._ratio
[docs] def set_ratios(self, mu, muf): self.set_bw_mean() self.set_bw_filt() self.set_ratio_stream(mu) self.set_ratio_filt(muf) self.set_ratio() return self._ratio
[docs] def is_valid(self, ratio, ratio_threshold=config.BRIDGE_RATIO_THRESHOLD): if not ratio: # logger.debug("No ratio for bridge %s", self.fingerprint) # If it has not been measured yet, it's valid if not self.latest_measurement(): logger.debug( "No measurements for bridge %s yet.", self.fingerprint ) return True, None # If none of the measurements was successful error = self.latest_measurement().error logger.debug( "No successful measurements for bridge %s: %s", self.fingerprint, error, ) return False, error if ratio < ratio_threshold: return False, None return True, None
[docs] def latest_measurement(self): try: latest_measurement = self.measurements.latest() except models.ObjectDoesNotExist: return None return latest_measurement