# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import random from heat.common import exception from heat.common.i18n import _ from heat.engine import constraints from heat.engine import properties from heat.engine import resource from heat.engine import support from oslo_utils import timeutils class Delay(resource.Resource): """A resource that pauses for a configurable delay. By manipulating the dependency relationships between resources in the template, a delay can be inserted at an arbitrary point during e.g. stack creation or deletion. They delay will occur after any resource that it depends on during CREATE or SUSPEND, and before any resource that it depends on during DELETE or RESUME. Similarly, it will occur before any resource that depends on it during CREATE or SUSPEND, and after any resource thet depends on it during DELETE or RESUME. If a non-zero maximum jitter is specified, a random amount of jitter - chosen with uniform probability in the range from 0 to the product of the maximum jitter value and the jitter multiplier (1s by default) - is added to the minimum delay time. This can be used, for example, in the scaled unit of a large scaling group to prevent 'thundering herd' issues. """ support_status = support.SupportStatus(version='11.0.0') _ALLOWED_ACTIONS = ( resource.Resource.CREATE, resource.Resource.DELETE, resource.Resource.SUSPEND, resource.Resource.RESUME, ) PROPERTIES = ( MIN_WAIT_SECS, MAX_JITTER, JITTER_MULTIPLIER_SECS, DELAY_ACTIONS, ) = ( 'min_wait', 'max_jitter', 'jitter_multiplier', 'actions', ) properties_schema = { MIN_WAIT_SECS: properties.Schema( properties.Schema.NUMBER, _('Minimum time in seconds to wait during the specified actions.'), update_allowed=True, default=0, constraints=[ constraints.Range(min=0) ] ), MAX_JITTER: properties.Schema( properties.Schema.NUMBER, _('Maximum jitter to add to the minimum wait time.'), update_allowed=True, default=0, constraints=[ constraints.Range(min=0), ] ), JITTER_MULTIPLIER_SECS: properties.Schema( properties.Schema.NUMBER, _('Number of seconds to multiply the maximum jitter value by.'), update_allowed=True, default=1.0, constraints=[ constraints.Range(min=0), ] ), DELAY_ACTIONS: properties.Schema( properties.Schema.LIST, _('Actions during which the delay will occur.'), update_allowed=True, default=[resource.Resource.CREATE], constraints=[constraints.AllowedValues(_ALLOWED_ACTIONS)] ), } attributes_schema = {} def _delay_parameters(self): """Return a tuple of the min delay and max jitter, in seconds.""" min_wait_secs = self.properties[self.MIN_WAIT_SECS] max_jitter_secs = (self.properties[self.MAX_JITTER] * self.properties[self.JITTER_MULTIPLIER_SECS]) return min_wait_secs, max_jitter_secs def validate(self): result = super(Delay, self).validate() if not self.stack.strict_validate: return result min_wait_secs, max_jitter_secs = self._delay_parameters() max_wait = min_wait_secs + max_jitter_secs if max_wait > self.stack.timeout_secs(): raise exception.StackValidationFailed(_('%(res_type)s maximum ' 'delay %(max_wait)ss ' 'exceeds stack timeout.') % {'res_type': self.type, 'max_wait': max_wait}) return result def _wait_secs(self, action): """Return a (randomised) wait time for the specified action.""" if action not in self.properties[self.DELAY_ACTIONS]: return 0 min_wait_secs, max_jitter_secs = self._delay_parameters() return min_wait_secs + (max_jitter_secs * random.random()) def _handle_action(self): """Return a tuple of the start time in UTC and the time to wait.""" return timeutils.utcnow(), self._wait_secs(self.action) @staticmethod def _check_complete(started_at, wait_secs): if not wait_secs: return True elapsed_secs = (timeutils.utcnow() - started_at).total_seconds() if elapsed_secs >= wait_secs: return True remaining = wait_secs - elapsed_secs if remaining >= 4: raise resource.PollDelay(int(remaining // 2)) return False def handle_create(self): return self._handle_action() def handle_delete(self): return self._handle_action() def handle_suspend(self): return self._handle_action() def handle_resume(self): return self._handle_action() def check_create_complete(self, cookie): return self._check_complete(*cookie) def check_delete_complete(self, cookie): return self._check_complete(*cookie) def check_suspend_complete(self, cookie): return self._check_complete(*cookie) def check_resume_complete(self, cookie): return self._check_status_complete(*cookie) def resource_mapping(): return { 'OS::Heat::Delay': Delay, }