Повышение надежности сервисов, устаревшего кода и устройств

Иногда код просто не так надежен, как хотелось бы. Возможно, вы имеете дело с внешней службой, которая время от времени не работает, или вы взаимодействуете с физическим устройством, которое не всегда в лучшем настроении, или вы имеете дело с устаревшей программой, которая самопроизвольно сгорает.

Какой бы ни была причина, часто вы можете значительно повысить надежность своего кода, используя повторные попытки.

Пример того, как это выглядит

Допустим, у нас есть простая функция, которая имеет 50%-й шанс отказа при вызове:

from random import choice

def unreliable_function():
  will_fail = choice([True, False])
  if will_fail:
    raise Exception("Oops! I failed")
  print("I succeeded!")

Повторяя эту функцию каждый раз, когда она терпит неудачу, мы можем увеличить вероятность того, что она в конечном итоге будет успешной. На самом деле, в зависимости от того, сколько раз мы повторим попытку, мы можем увеличить вероятность успеха до желаемого уровня.

Так как же это выглядит на самом деле? Что-то вроде этого:

def retry(unreliable_function):
  attempts = 0
  while True:
    try:
      unreliable_function()
      break
    except Exception:
      pass
    finally:
      attempts += 1
      
  

Теперь, вместо прямого запуска unreliable_function(), просто запустите retry(unreliable_function), и ваши шансы на успешное выполнение функции увеличатся с 50% до почти 100% (хотя иногда вам, возможно, придется немного подождать).

Используйте максимальное количество попыток

Обычно рекомендуется в конечном итоге потерпеть неудачу, если ненадежная функция просто не будет работать. Это может быть связано с тем, как вы с ним взаимодействуете, или рассматриваемая служба может быть полностью отключена, или программа полностью сломана.

Какой бы ни была причина, вы обычно хотите знать, если она неоднократно выходит из строя выше некоторого предопределенного порога.

По этой причине обычно рекомендуется устанавливать максимальное количество повторных попыток. Число, которое вы используете, будет зависеть от того, насколько ненадежен ваш код и насколько вы хотите гарантировать, что он будет успешным. Например, если код имеет вероятность ошибки 50 %, а вы хотите, чтобы вероятность успеха составляла 99,999 %, вам следует установить максимальное количество повторных попыток равным 20. Вот модифицированный пример, использующий максимальное количество повторных попыток:

def retry(unreliable_function, max_retries=20):
  attempts = 0
  while attempts < max_retries:
    try:
      unreliable_function()
      break
    except Exception:
      pass
    finally:
      attempts += 1

Использование тайм-аутов

Обычно рекомендуется подождать между повторными попытками. Например, вы пытаетесь получить доступ к службе, для прогрева которой требуется некоторое время, или к устройству, для загрузки которого требуется некоторое время, или к программе, для инициализации которой требуется некоторое время. Для функции не характерно иметь фиксированную вероятность отказа — иногда это так, но обычно она дает сбой кучу раз подряд, а затем удается.

В этих случаях рекомендуется добавить несколько тайм-аутов в функцию повтора:

import time

def retry(unreliable_function, max_retries=20):
  attempts = 0
  while attempts < max_retries:
    try:
      unreliable_function()
      break
    except Exception:
      pass
    finally:
      attempts += 1
      time.sleep(2)

Использование экспоненциального отката

Если вы не уверены, как долго вам придется ждать, и хотите избежать перегрузок ненадежной функции или службы попытками, вы можете использовать экспоненциальную отсрочку для решения этой проблемы, которая увеличивает количество времени. некоторым фактором после каждой неудачи. По сути, это говорит: «Ну, может быть, мы просто не уделили сервису достаточно времени. Давай уделим этому больше времени, чем в прошлый раз».

Это отличный компромисс, если доступ к ненадежному сервису требует больших затрат, и вы не просто хотите спамить его, но и не знаете, сколько времени потребуется, чтобы ответить. В этом случае мы могли бы изменить нашу функцию повтора, чтобы она выглядела примерно так:

import time

def retry(function, max_attempts=5, increase_factor=2):
  attempts = 0
  while attempts < max_attempts:
    try:
      unreliable_function()
      break
    except Exception:
      pass
    finally:
      base_time_to_sleep = 2
      time_to_sleep = (increase_factor ** attempts) * base_time_to_sleep
      attempts += 1
      time.sleep(time_to_sleep)

Это вызовет ненадежную функцию, затем подождите 2 секунды, прежде чем вызывать ее снова, затем подождите 4 секунды, затем 8, затем 16 и так далее.

Побочные эффекты — плохая идея

При повторной попытке кода рекомендуется по возможности избегать побочных эффектов кода, который вы вызываете. Побочные эффекты могут привести к тому, что другие части программы будут вести себя так, как если бы ваша ненадежная функция или программа выполнились успешно.

Идемпотентность - хорошая идея

Если вы не можете избежать побочных эффектов от кода, который вы будете запускать, было бы действительно хорошей идеей убедиться, что эти побочные эффекты являются идемпотентными, то есть код, вызывающий нарушение, может выполняться несколько раз, и побочные эффекты не будут суммироваться. или взаимодействовать. Хорошим примером этого является установка переменной в качестве побочного эффекта:

class UnreliableClass:
  def unreliable_method():
    self.side_effect_variable = 4

Приведенный выше код содержит побочный эффект, но этот побочный эффект является идемпотентным. Независимо от того, сколько раз этот код будет выполняться, он всегда будет вызывать одни и те же результаты. Сравните это с кодом ниже:

class UnreliableClass

  def unreliable_method():
    self.side_effect_variable = self.side_effect_variable + 4

Это неидемпотент. Каждый раз, когда вы выполняете этот код, side_effect_variable будет иметь другое значение. Это может вызвать серьезные проблемы, так как ваш код будет не столько «повторно», сколько «составлен».

Заключительные мысли

Есть много направлений для повторных попыток. Вы можете декорировать функции вместо прямого вызова retry(), превращать их в асинхронные функции, чтобы вам не приходилось загружать основной цикл управления, и так далее.

Но основы довольно просты, и это чрезвычайно полезная техника для превращения ненадежного кода в более надежный код.