1# Copyright 2011 OpenStack Foundation. 2# All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15 16""" 17Time related utilities and helper functions. 18""" 19 20import calendar 21import datetime 22import functools 23import logging 24import time 25 26from debtcollector import removals 27import iso8601 28import pytz 29 30from oslo_utils import reflection 31 32# ISO 8601 extended time format with microseconds 33_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' 34_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' 35PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND 36 37_MAX_DATETIME_SEC = 59 38 39now = time.monotonic 40 41 42@removals.remove( 43 message="use datetime.datetime.isoformat()", 44 version="1.6", 45 removal_version="?", 46 ) 47def isotime(at=None, subsecond=False): 48 """Stringify time in ISO 8601 format. 49 50 .. deprecated:: 1.5.0 51 Use :func:`utcnow` and :func:`datetime.datetime.isoformat` instead. 52 """ 53 if not at: 54 at = utcnow() 55 st = at.strftime(_ISO8601_TIME_FORMAT 56 if not subsecond 57 else _ISO8601_TIME_FORMAT_SUBSECOND) 58 tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' 59 # Need to handle either iso8601 or python UTC format 60 st += ('Z' if tz in ('UTC', 'UTC+00:00') else tz) 61 return st 62 63 64def parse_isotime(timestr): 65 """Parse time from ISO 8601 format.""" 66 try: 67 return iso8601.parse_date(timestr) 68 except iso8601.ParseError as e: 69 raise ValueError(str(e)) 70 except TypeError as e: 71 raise ValueError(str(e)) 72 73 74@removals.remove( 75 message="use either datetime.datetime.isoformat() " 76 "or datetime.datetime.strftime() instead", 77 version="1.6", 78 removal_version="?", 79 ) 80def strtime(at=None, fmt=PERFECT_TIME_FORMAT): 81 """Returns formatted utcnow. 82 83 .. deprecated:: 1.5.0 84 Use :func:`utcnow()`, :func:`datetime.datetime.isoformat` 85 or :func:`datetime.strftime` instead: 86 87 * ``strtime()`` => ``utcnow().isoformat()`` 88 * ``strtime(fmt=...)`` => ``utcnow().strftime(fmt)`` 89 * ``strtime(at)`` => ``at.isoformat()`` 90 * ``strtime(at, fmt)`` => ``at.strftime(fmt)`` 91 """ 92 if not at: 93 at = utcnow() 94 return at.strftime(fmt) 95 96 97def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): 98 """Turn a formatted time back into a datetime.""" 99 return datetime.datetime.strptime(timestr, fmt) 100 101 102def normalize_time(timestamp): 103 """Normalize time in arbitrary timezone to UTC naive object.""" 104 offset = timestamp.utcoffset() 105 if offset is None: 106 return timestamp 107 return timestamp.replace(tzinfo=None) - offset 108 109 110def is_older_than(before, seconds): 111 """Return True if before is older than seconds. 112 113 .. versionchanged:: 1.7 114 Accept datetime string with timezone information. 115 Fix comparison with timezone aware datetime. 116 """ 117 if isinstance(before, str): 118 before = parse_isotime(before) 119 120 before = normalize_time(before) 121 122 return utcnow() - before > datetime.timedelta(seconds=seconds) 123 124 125def is_newer_than(after, seconds): 126 """Return True if after is newer than seconds. 127 128 .. versionchanged:: 1.7 129 Accept datetime string with timezone information. 130 Fix comparison with timezone aware datetime. 131 """ 132 if isinstance(after, str): 133 after = parse_isotime(after) 134 135 after = normalize_time(after) 136 137 return after - utcnow() > datetime.timedelta(seconds=seconds) 138 139 140def utcnow_ts(microsecond=False): 141 """Timestamp version of our utcnow function. 142 143 See :py:class:`oslo_utils.fixture.TimeFixture`. 144 145 .. versionchanged:: 1.3 146 Added optional *microsecond* parameter. 147 """ 148 if utcnow.override_time is None: 149 # NOTE(kgriffs): This is several times faster 150 # than going through calendar.timegm(...) 151 timestamp = time.time() 152 if not microsecond: 153 timestamp = int(timestamp) 154 return timestamp 155 156 now = utcnow() 157 timestamp = calendar.timegm(now.timetuple()) 158 159 if microsecond: 160 timestamp += float(now.microsecond) / 1000000 161 162 return timestamp 163 164 165def utcnow(with_timezone=False): 166 """Overridable version of utils.utcnow that can return a TZ-aware datetime. 167 168 See :py:class:`oslo_utils.fixture.TimeFixture`. 169 170 .. versionchanged:: 1.6 171 Added *with_timezone* parameter. 172 """ 173 if utcnow.override_time: 174 try: 175 return utcnow.override_time.pop(0) 176 except AttributeError: 177 return utcnow.override_time 178 if with_timezone: 179 return datetime.datetime.now(tz=iso8601.iso8601.UTC) 180 return datetime.datetime.utcnow() 181 182 183@removals.remove( 184 message="use datetime.datetime.utcfromtimestamp().isoformat()", 185 version="1.6", 186 removal_version="?", 187 ) 188def iso8601_from_timestamp(timestamp, microsecond=False): 189 """Returns an iso8601 formatted date from timestamp. 190 191 .. versionchanged:: 1.3 192 Added optional *microsecond* parameter. 193 194 .. deprecated:: 1.5.0 195 Use :func:`datetime.datetime.utcfromtimestamp` and 196 :func:`datetime.datetime.isoformat` instead. 197 """ 198 return isotime(datetime.datetime.utcfromtimestamp(timestamp), microsecond) 199 200 201utcnow.override_time = None 202 203 204def set_time_override(override_time=None): 205 """Overrides utils.utcnow. 206 207 Make it return a constant time or a list thereof, one at a time. 208 209 See :py:class:`oslo_utils.fixture.TimeFixture`. 210 211 :param override_time: datetime instance or list thereof. If not 212 given, defaults to the current UTC time. 213 """ 214 utcnow.override_time = override_time or datetime.datetime.utcnow() 215 216 217def advance_time_delta(timedelta): 218 """Advance overridden time using a datetime.timedelta. 219 220 See :py:class:`oslo_utils.fixture.TimeFixture`. 221 222 """ 223 assert utcnow.override_time is not None # nosec 224 try: 225 for dt in utcnow.override_time: 226 dt += timedelta 227 except TypeError: 228 utcnow.override_time += timedelta 229 230 231def advance_time_seconds(seconds): 232 """Advance overridden time by seconds. 233 234 See :py:class:`oslo_utils.fixture.TimeFixture`. 235 236 """ 237 advance_time_delta(datetime.timedelta(0, seconds)) 238 239 240def clear_time_override(): 241 """Remove the overridden time. 242 243 See :py:class:`oslo_utils.fixture.TimeFixture`. 244 245 """ 246 utcnow.override_time = None 247 248 249def marshall_now(now=None): 250 """Make an rpc-safe datetime with microseconds. 251 252 .. versionchanged:: 1.6 253 Timezone information is now serialized instead of being stripped. 254 """ 255 if not now: 256 now = utcnow() 257 d = dict(day=now.day, month=now.month, year=now.year, hour=now.hour, 258 minute=now.minute, second=now.second, 259 microsecond=now.microsecond) 260 if now.tzinfo: 261 # Need to handle either iso8601 or python UTC format 262 tzname = now.tzinfo.tzname(None) 263 d['tzname'] = 'UTC' if tzname == 'UTC+00:00' else tzname 264 return d 265 266 267def unmarshall_time(tyme): 268 """Unmarshall a datetime dict. 269 270 .. versionchanged:: 1.5 271 Drop leap second. 272 273 .. versionchanged:: 1.6 274 Added support for timezone information. 275 """ 276 277 # NOTE(ihrachys): datetime does not support leap seconds, 278 # so the best thing we can do for now is dropping them 279 # http://bugs.python.org/issue23574 280 second = min(tyme['second'], _MAX_DATETIME_SEC) 281 dt = datetime.datetime(day=tyme['day'], 282 month=tyme['month'], 283 year=tyme['year'], 284 hour=tyme['hour'], 285 minute=tyme['minute'], 286 second=second, 287 microsecond=tyme['microsecond']) 288 tzname = tyme.get('tzname') 289 if tzname: 290 # Need to handle either iso8601 or python UTC format 291 tzname = 'UTC' if tzname == 'UTC+00:00' else tzname 292 tzinfo = pytz.timezone(tzname) 293 dt = tzinfo.localize(dt) 294 return dt 295 296 297def delta_seconds(before, after): 298 """Return the difference between two timing objects. 299 300 Compute the difference in seconds between two date, time, or 301 datetime objects (as a float, to microsecond resolution). 302 """ 303 delta = after - before 304 return delta.total_seconds() 305 306 307def is_soon(dt, window): 308 """Determines if time is going to happen in the next window seconds. 309 310 :param dt: the time 311 :param window: minimum seconds to remain to consider the time not soon 312 313 :return: True if expiration is within the given duration 314 """ 315 soon = (utcnow() + datetime.timedelta(seconds=window)) 316 return normalize_time(dt) <= soon 317 318 319class Split(object): 320 """A *immutable* stopwatch split. 321 322 See: http://en.wikipedia.org/wiki/Stopwatch for what this is/represents. 323 324 .. versionadded:: 1.4 325 """ 326 327 __slots__ = ['_elapsed', '_length'] 328 329 def __init__(self, elapsed, length): 330 self._elapsed = elapsed 331 self._length = length 332 333 @property 334 def elapsed(self): 335 """Duration from stopwatch start.""" 336 return self._elapsed 337 338 @property 339 def length(self): 340 """Seconds from last split (or the elapsed time if no prior split).""" 341 return self._length 342 343 def __repr__(self): 344 r = reflection.get_class_name(self, fully_qualified=False) 345 r += "(elapsed=%s, length=%s)" % (self._elapsed, self._length) 346 return r 347 348 349def time_it(logger, log_level=logging.DEBUG, 350 message="It took %(seconds).02f seconds to" 351 " run function '%(func_name)s'", 352 enabled=True, min_duration=0.01): 353 """Decorator that will log how long its decorated function takes to run. 354 355 This does **not** output a log if the decorated function fails 356 with an exception. 357 358 :param logger: logger instance to use when logging elapsed time 359 :param log_level: logger logging level to use when logging elapsed time 360 :param message: customized message to use when logging elapsed time, 361 the message may use automatically provide values 362 ``%(seconds)`` and ``%(func_name)`` if it finds those 363 values useful to record 364 :param enabled: whether to enable or disable this decorator (useful to 365 decorate a function with this decorator, and then easily 366 be able to switch that decoration off by some config or 367 other value) 368 :param min_duration: argument that determines if logging is triggered 369 or not, it is by default set to 0.01 seconds to avoid 370 logging when durations and/or elapsed function call 371 times are less than 0.01 seconds, to disable 372 any ``min_duration`` checks this value should be set 373 to less than or equal to zero or set to none 374 """ 375 376 def decorator(func): 377 if not enabled: 378 return func 379 380 @functools.wraps(func) 381 def wrapper(*args, **kwargs): 382 with StopWatch() as w: 383 result = func(*args, **kwargs) 384 time_taken = w.elapsed() 385 if min_duration is None or time_taken >= min_duration: 386 logger.log(log_level, message, 387 {'seconds': time_taken, 388 'func_name': reflection.get_callable_name(func)}) 389 return result 390 391 return wrapper 392 393 return decorator 394 395 396class StopWatch(object): 397 """A simple timer/stopwatch helper class. 398 399 Inspired by: apache-commons-lang java stopwatch. 400 401 Not thread-safe (when a single watch is mutated by multiple threads at 402 the same time). Thread-safe when used by a single thread (not shared) or 403 when operations are performed in a thread-safe manner on these objects by 404 wrapping those operations with locks. 405 406 It will use the `monotonic`_ pypi library to find an appropriate 407 monotonically increasing time providing function (which typically varies 408 depending on operating system and python version). 409 410 .. _monotonic: https://pypi.org/project/monotonic/ 411 412 .. versionadded:: 1.4 413 """ 414 _STARTED = 'STARTED' 415 _STOPPED = 'STOPPED' 416 417 def __init__(self, duration=None): 418 if duration is not None and duration < 0: 419 raise ValueError("Duration must be greater or equal to" 420 " zero and not %s" % duration) 421 self._duration = duration 422 self._started_at = None 423 self._stopped_at = None 424 self._state = None 425 self._splits = () 426 427 def start(self): 428 """Starts the watch (if not already started). 429 430 NOTE(harlowja): resets any splits previously captured (if any). 431 """ 432 if self._state == self._STARTED: 433 return self 434 self._started_at = now() 435 self._stopped_at = None 436 self._state = self._STARTED 437 self._splits = () 438 return self 439 440 @property 441 def splits(self): 442 """Accessor to all/any splits that have been captured.""" 443 return self._splits 444 445 def split(self): 446 """Captures a split/elapsed since start time (and doesn't stop).""" 447 if self._state == self._STARTED: 448 elapsed = self.elapsed() 449 if self._splits: 450 length = self._delta_seconds(self._splits[-1].elapsed, elapsed) 451 else: 452 length = elapsed 453 self._splits = self._splits + (Split(elapsed, length),) 454 return self._splits[-1] 455 else: 456 raise RuntimeError("Can not create a split time of a stopwatch" 457 " if it has not been started or if it has been" 458 " stopped") 459 460 def restart(self): 461 """Restarts the watch from a started/stopped state.""" 462 if self._state == self._STARTED: 463 self.stop() 464 self.start() 465 return self 466 467 @staticmethod 468 def _delta_seconds(earlier, later): 469 # Uses max to avoid the delta/time going backwards (and thus negative). 470 return max(0.0, later - earlier) 471 472 def elapsed(self, maximum=None): 473 """Returns how many seconds have elapsed.""" 474 if self._state not in (self._STARTED, self._STOPPED): 475 raise RuntimeError("Can not get the elapsed time of a stopwatch" 476 " if it has not been started/stopped") 477 if self._state == self._STOPPED: 478 elapsed = self._delta_seconds(self._started_at, self._stopped_at) 479 else: 480 elapsed = self._delta_seconds(self._started_at, now()) 481 if maximum is not None and elapsed > maximum: 482 elapsed = max(0.0, maximum) 483 return elapsed 484 485 def __enter__(self): 486 """Starts the watch.""" 487 self.start() 488 return self 489 490 def __exit__(self, type, value, traceback): 491 """Stops the watch (ignoring errors if stop fails).""" 492 try: 493 self.stop() 494 except RuntimeError: # nosec: errors are meant to be ignored 495 pass 496 497 def leftover(self, return_none=False): 498 """Returns how many seconds are left until the watch expires. 499 500 :param return_none: when ``True`` instead of raising a ``RuntimeError`` 501 when no duration has been set this call will 502 return ``None`` instead. 503 :type return_none: boolean 504 """ 505 if self._state != self._STARTED: 506 raise RuntimeError("Can not get the leftover time of a stopwatch" 507 " that has not been started") 508 if self._duration is None: 509 if not return_none: 510 raise RuntimeError("Can not get the leftover time of a watch" 511 " that has no duration") 512 return None 513 return max(0.0, self._duration - self.elapsed()) 514 515 def expired(self): 516 """Returns if the watch has expired (ie, duration provided elapsed).""" 517 if self._state not in (self._STARTED, self._STOPPED): 518 raise RuntimeError("Can not check if a stopwatch has expired" 519 " if it has not been started/stopped") 520 if self._duration is None: 521 return False 522 return self.elapsed() > self._duration 523 524 def has_started(self): 525 """Returns True if the watch is in a started state.""" 526 return self._state == self._STARTED 527 528 def has_stopped(self): 529 """Returns True if the watch is in a stopped state.""" 530 return self._state == self._STOPPED 531 532 def resume(self): 533 """Resumes the watch from a stopped state.""" 534 if self._state == self._STOPPED: 535 self._state = self._STARTED 536 return self 537 else: 538 raise RuntimeError("Can not resume a stopwatch that has not been" 539 " stopped") 540 541 def stop(self): 542 """Stops the watch.""" 543 if self._state == self._STOPPED: 544 return self 545 if self._state != self._STARTED: 546 raise RuntimeError("Can not stop a stopwatch that has not been" 547 " started") 548 self._stopped_at = now() 549 self._state = self._STOPPED 550 return self 551