def test_reconfiguration_lock(self): mlos_object = MlosObject(int, int) assert not mlos_object._reconfiguration_lock._is_owned() with mlos_object.reconfiguration_lock(): assert mlos_object._reconfiguration_lock._is_owned() assert not mlos_object._reconfiguration_lock._is_owned()
def test_reconfiguration_lock(self): mlos_object = MlosObject(int, int) self.assertFalse(mlos_object._reconfiguration_lock._is_owned()) with mlos_object.reconfiguration_lock(): self.assertTrue(mlos_object._reconfiguration_lock._is_owned()) self.assertFalse(mlos_object._reconfiguration_lock._is_owned())
def __init__(self, logger): self.logger = logger self.current_config = None # Let's use an mlos_object to drive the runtime configuration of the cache self.mlos_object = MlosObject( smart_component_type=type(self), smart_component_runtime_attributes=self.RuntimeAttributes( component_id=id(self))) self.mlos_object.register() self.reconfigure()
def __init__(self, logger): self.logger = logger self.mlos_object = MlosObject( smart_component_type=type(self), smart_component_runtime_attributes=self.RuntimeAttributes( component_id=id(self))) self.current_config = Configuration(component_type=SmartCache, values=self.default_config, id=-1) self.cache_implementation = LruCache( max_size=self.current_config.values.lru_cache_config.cache_size, logger=self.logger) self.mlos_object.register() self.reconfigure()
class SmartCacheWorkloadGenerator: """Instantiates one (or many) SmartCaches and subjects them to various workloads. The main idea here is to exercise all use-cases demanded of Mlos. Parameters ---------- logger : Logger Logger. Attributes ---------- RuntimeAttributes : MlosSmartComponentRuntimeAttributes default_config : Point telemetry_message_types : list mlos_object : MlosObject """ RuntimeAttributes = MlosSmartComponentRuntimeAttributes( smart_component_name="SmartCacheWorkloadGenerator", attribute_names=[]) default_config = smart_cache_workload_generator_default_config parameter_search_space = smart_cache_workload_generator_config_space telemetry_message_types = [] def __init__(self, logger): self.logger = logger self.current_config = None # Let's use an mlos_object to drive the runtime configuration of the cache self.mlos_object = MlosObject( smart_component_type=type(self), smart_component_runtime_attributes=self.RuntimeAttributes( component_id=id(self))) self.mlos_object.register() self.reconfigure() def __del__(self): self.mlos_object.unregister() def reconfigure(self): new_config = self.mlos_object.config if new_config == self.current_config: return False self.mlos_object.send_telemetry_message( SmartCacheWorkloadGeneratorReconfigure( old_config_id=self.current_config.id if self.current_config is not None else 0, new_config_id=new_config.id)) self.current_config = new_config return True def run(self, timeout_s=1): self.logger.info( f"Started the SmartCacheWorkloadGenerator. Duration={timeout_s}") self.reconfigure() smart_cache = SmartCache(logger=self.logger) start_time = datetime.datetime.utcnow() end_time = start_time + datetime.timedelta(seconds=timeout_s) while datetime.datetime.utcnow() < end_time: if self.current_config.values.workload_type == 'fibonacci': range_min = self.current_config.values.fibonacci_config.min range_max = range_min + self.current_config.values.fibonacci_config.range_width while datetime.datetime.utcnow() < end_time: sequence_number = random.randint(range_min, range_max) self.logger.debug(f"\tfib({sequence_number}) = ?") result = self.fibonacci(sequence_number, smart_cache) self.logger.debug(f"\tfib({sequence_number}) = {result}") elif self.current_config.values.workload_type == 'random_key_from_range': range_min = self.current_config.values.random_key_from_range_config.min range_max = range_min + self.current_config.values.random_key_from_range_config.range_width while datetime.datetime.utcnow() < end_time: key = random.randint(range_min, range_max) value = smart_cache.get(key) if value is None: value = str(key) smart_cache.push(key, value) elif self.current_config.values.workload_type == 'sequential_key_from_range': range_min = self.current_config.values.sequential_key_from_range_config.min range_width = self.current_config.values.sequential_key_from_range_config.range_width i = 0 while datetime.datetime.utcnow() < end_time: key = range_min + (i % range_width) value = smart_cache.get(key) if value is None: value = str(key) smart_cache.push(key, value) i += 1 else: raise RuntimeError( f"Unknown workload type: {self.current_config.values.workload_type}" ) self.logger.info("Exiting the SmartCacheWorkloadGenerator.") class _FibonacciValue: def __init__(self, sequence_number, value, previous): self.sequence_number = sequence_number self.value = value self.previous = previous def fibonacci(self, sequence_number, smart_cache): existing_result = smart_cache.get(sequence_number) max_key_up_to = None if existing_result is None: all_cached_smaller_keys = [ element.key for element in smart_cache if element.key < sequence_number ] if all_cached_smaller_keys: max_key_up_to = max(all_cached_smaller_keys) start = 1 previous = 1 current = 1 if max_key_up_to is not None: cached_value = smart_cache.get(max_key_up_to) if cached_value is not None: previous = cached_value.previous current = cached_value.value start = cached_value.sequence_number for i in range(start + 1, sequence_number): previous, current = current, previous + current smart_cache.push(key=i, value=self._FibonacciValue(sequence_number=i, value=current, previous=previous)) return current
class SmartCache: """ A tunable and observable cache that takes advantage of Mlos. The goal here is to provide a bunch of cache implementations that are parameterizable. Parameters ---------- logger : Logger Logger to use. Attributes ---------- RuntimeAttributes : MlosSmartComponentRuntimeAttributes parameter_search_space : SimpleHypergrid default_config : Point telemetry_message_types : list runtime_decision_contexts : list """ # Used during registration RuntimeAttributes = MlosSmartComponentRuntimeAttributes( smart_component_name="SmartCache", attribute_names=[]) # Used to inform the Mlos Global Context about all types of telemetry messages that this component can emit telemetry_message_types = [ (SmartCachePush, 0b1), (SmartCacheGet, 0b10), (SmartCacheEvict, 0b100), ] # Used to inform the Mlos Global Context about all types of runtime decisions that can be expected runtime_decision_contexts = [ PushRuntimeDecisionContext, ReconfigurationRuntimeDecisionContext, ] default_config = smart_cache_config_store.default parameter_search_space = smart_cache_config_store.parameter_space def __init__(self, logger): self.logger = logger self.mlos_object = MlosObject( smart_component_type=type(self), smart_component_runtime_attributes=self.RuntimeAttributes( component_id=id(self))) self.current_config = Configuration( component_type=SmartCache, values=smart_cache_config_store.default, id=-1) self.cache_implementation = LruCache( max_size=self.current_config.values.lru_cache_config.cache_size, logger=self.logger) self.mlos_object.register() self.reconfigure() def __del__(self): self.mlos_object.unregister() def __iter__(self): return self.cache_implementation.__iter__() def __len__(self): return len(self.cache_implementation) def __contains__(self, item): return item in self.cache_implementation def push(self, key, value): self.reconfigure() # TODO: make this less frequent if key in self: return should_push = self.mlos_object.make_runtime_decision( PushRuntimeDecisionContext(mlos_object=self.mlos_object, current_config=self.current_config)) if not should_push: return if self.mlos_object.is_message_type_enabled(SmartCachePush): # Note that we hide this behind an 'is_enabled' check. This is for the cases # when assembling the message itself can be expensive. self.mlos_object.send_telemetry_message(SmartCachePush(key=key)) cache_entry = CacheEntry(key, value) evicted_cache_entry = self.cache_implementation.push(cache_entry) if evicted_cache_entry is not None: # Note that here we skip the 'is message type enabled check' since assembling the message is cheap and # the check can be done by mlos_object self.mlos_object.send_telemetry_message( SmartCacheEvict(key=evicted_cache_entry.key)) def get(self, key): if key not in self.cache_implementation: self.mlos_object.send_telemetry_message( SmartCacheGet(key=key, was_hit=False)) return None self.mlos_object.send_telemetry_message( SmartCacheGet(key=key, was_hit=True)) return self.cache_implementation.get(key) def reconfigure(self): """ Reconfigures the cache according to the configuration present in self.mlos_object :return: """ smart_cache_reconfiguration_decision_runtime_context = ReconfigurationRuntimeDecisionContext( self.mlos_object) should_reconfigure = self.mlos_object.make_runtime_decision( smart_cache_reconfiguration_decision_runtime_context) if not should_reconfigure or self.current_config == self.mlos_object.config or self.mlos_object.config is None: return self.current_config = self.mlos_object.config self.logger.debug( f"Reconfiguring. New config values: {self.current_config.values.to_json()}" ) if self.current_config.values.implementation == 'LRU': self.cache_implementation = LruCache( max_size=self.current_config.values.lru_cache_config. cache_size, logger=self.logger) elif self.current_config.values.implementation == 'MRU': self.cache_implementation = MruCache( max_size=self.current_config.values.mru_cache_config. cache_size, logger=self.logger) else: raise RuntimeError("Invalid config")