Skip to content

Remove retry circular dependency #1969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 47 additions & 30 deletions common/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
# limitations under the License.
"""Set up for logging."""
from enum import Enum

import logging
import os
import sys
import traceback
import time
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoying google nit, can you move this line up so the imports are alphabetical order


import google.cloud.logging
from google.cloud.logging_v2.handlers.handlers import CloudLoggingHandler
Expand All @@ -25,8 +27,8 @@
# Disable this check since we have a bunch of non-constant globals in this file.
# pylint: disable=invalid-name

from common import retry
from common import utils
from common.retry import get_delay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annother annoying Google nit:we don't import functions, only modules:
(see https://google.github.io/styleguide/pyguide.html)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, got it!

I did this way to avoid logs importing retry and retry importing logs, not sure if it is a problem since log is not using retry.wrap decorator though, but I wanted to reuse the get_delay function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THis is a good point. I think we should just reimplement get_delay here instead of having circular imports.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or actually, can you move it to a third module that both modules can import?


_default_logger = None
_log_client = None
Expand All @@ -37,6 +39,7 @@

NUM_RETRIES = 5
RETRY_DELAY = 1
BACKOFF = 2


def _initialize_cloud_clients():
Expand Down Expand Up @@ -153,47 +156,61 @@ class LogSeverity(Enum):
DEBUG = logging.DEBUG


@retry.wrap(NUM_RETRIES, RETRY_DELAY, 'common.logs.log', log_retries=False)
def log(logger, severity, message, *args, extras=None):
"""Log a message with severity |severity|. If using stack driver logging
then |extras| is also logged (in addition to default extras)."""
message = str(message)
if args:
message = message % args

if utils.is_local():
if extras:
message += ' Extras: ' + str(extras)
logging.log(severity, message)
return

if logger is None:
logger = _default_logger
assert logger

struct_message = {
'message': message,
}
all_extras = _default_extras.copy()
extras = extras or {}
all_extras.update(extras)
struct_message.update(all_extras)
severity = LogSeverity(severity).name
logger.log_struct(struct_message, severity=severity)
# Custom retry logic to avoid circular dependency
# as retry from retry.py uses log
for num_try in range(1, NUM_RETRIES + 1):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do this from 0? Or change NUM_RETRIES to NUM_ATTEMPTS?
As far as I can tell this will run the loop a max of 3 times. but num_retries=3 technically implies a max of 4.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I decided to change the nomenclature to NUM_ATTEMPTS, as the get_retry function already was 1-index based

try:
message = str(message)
if args:
message = message % args

if utils.is_local():
if extras:
message += ' Extras: ' + str(extras)
logging.log(severity, message)
return

if logger is None:
logger = _default_logger
assert logger

struct_message = {
'message': message,
}
all_extras = _default_extras.copy()
extras = extras or {}
all_extras.update(extras)
struct_message.update(all_extras)
severity = LogSeverity(severity).name
logger.log_struct(struct_message, severity=severity)
break
except Exception: # pylint: disable=broad-except
# We really dont want do to do anything here except sleep here,
# since we cant log it out as log itself is already failing
time.sleep(get_delay(num_try, RETRY_DELAY, BACKOFF))


def error(message, *args, extras=None, logger=None):
"""Logs |message| to stackdriver logging and error reporting (including
exception if there was one."""

@retry.wrap(NUM_RETRIES,
RETRY_DELAY,
'common.logs.error._report_error_with_retries',
log_retries=False)
def _report_error_with_retries(message):
if utils.is_local():
return
_error_reporting_client.report(message)

# Custom retry logic to avoid circular dependency
# as retry from retry.py uses log
for num_try in range(1, NUM_RETRIES + 1):
try:
_error_reporting_client.report(message)
break
except Exception: # pylint: disable=broad-except
# We really dont want do to do anything here except sleep here,
# since we cant log it out as log itself is already failing
time.sleep(get_delay(num_try, RETRY_DELAY, BACKOFF))

if not any(sys.exc_info()):
_report_error_with_retries(message % args)
Expand Down
12 changes: 5 additions & 7 deletions common/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import sys
import time

from common import logs


def sleep(seconds):
"""Invoke time.sleep."""
Expand All @@ -37,11 +39,8 @@ def wrap( # pylint: disable=too-many-arguments
function,
backoff=2,
exception_type=Exception,
log_retries=True,
retry_on_false=False):
"""Retry decorator for a function."""
# To avoid circular dependency.
from common import logs # pylint:disable=import-outside-toplevel

assert delay > 0
assert backoff >= 1
Expand All @@ -60,10 +59,9 @@ def handle_retry(num_try, exception=None):

if (exception is None or
isinstance(exception, exception_type)) and num_try < tries:
if log_retries:
logs.info('Retrying on %s failed with %s. Retrying again.',
function_with_type,
sys.exc_info()[1])
logs.info('Retrying on %s failed with %s. Retrying again.',
function_with_type,
sys.exc_info()[1])
sleep(get_delay(num_try, delay, backoff))
return True

Expand Down