Files
Anthias/lib/github.py
Nash Kaminski a2cf9956bd Improve error handling when interacting with the Github API.
This change improves the error handling when interacting with the github
API by enforcing a 5 minute backoff on API call failures as well as
caches the remote branch presence/absence for 24 hours.

This prevents potential crashes when Internet connectivity is
unavailable, as well as from having one's IP address rate limited by Github
if the remote branch on the pi references a branch name that does not exist
under https://github.com/Screenly/screenly-ose/tree/

Prior to this change, this scenario causes the remote branch status to be
checked during every UI request.

Signed-off-by: Nash Kaminski <nashkaminski@kaminski.io>
2021-10-01 19:53:26 -05:00

151 lines
5.0 KiB
Python

import os
import logging
import string
import random
from requests import get as requests_get, exceptions
from lib.utils import is_balena_app, is_docker, is_ci, connect_to_redis
from lib.diagnostics import get_git_branch, get_git_hash, get_git_short_hash
from lib.raspberry_pi_helper import parse_cpu_info, lookup_raspberry_pi_revision
from mixpanel import Mixpanel, MixpanelException
from settings import settings
r = connect_to_redis()
# Availability and HEAD commit of the remote branch to be checked every 24 hours.
REMOTE_BRANCH_STATUS_TTL = (60 * 60 * 24)
# Suspend all external requests if we enconter an error other than a ConnectionError for 5 minutes
ERROR_BACKOFF_TTL = (60 * 5)
def handle_github_error(exc, action):
# After failing, dont retry until backoff timer expires
r.set('github-api-error', action)
r.expire('github-api-error', ERROR_BACKOFF_TTL)
# Print a useful error message
if exc.response:
errdesc = exc.response.content
else:
errdesc = 'no data'
logging.error('{} fetching {} from GitHub: {}'.format(type(exc).__name__, action, errdesc))
def remote_branch_available(branch):
if not branch:
logging.error('No branch specified. Exiting.')
return None
# Make sure we havent recently failed before allowing fetch
if r.get('github-api-error') is not None:
logging.warning("GitHub requests suspended due to prior error")
return None
# Check for cached remote branch status
remote_branch_cache = r.get('remote-branch-available')
if remote_branch_cache is not None:
return remote_branch_cache == "1"
try:
resp = requests_get(
'https://api.github.com/repos/screenly/screenly-ose/branches',
headers={
'Accept': 'application/vnd.github.loki-preview+json',
},
)
resp.raise_for_status()
except exceptions.RequestException as exc:
handle_github_error(exc, 'remote branch availability')
return None
found = False
for github_branch in resp.json():
if github_branch['name'] == branch:
found = True
break
# Cache and return the result
if found:
r.set('remote-branch-available', '1')
else:
r.set('remote-branch-available', '0')
r.expire('remote-branch-available', REMOTE_BRANCH_STATUS_TTL)
return found
def fetch_remote_hash():
"""
Returns both the hash and if the status was updated
or not.
"""
branch = os.getenv('GIT_BRANCH')
if not branch:
logging.error('Unable to get local Git branch')
return None, False
get_cache = r.get('latest-remote-hash')
if not get_cache:
# Ensure the remote branch is available before trying to fetch the HEAD ref
if not remote_branch_available(branch):
logging.error('Remote Git branch not available')
return None, False
try:
resp = requests_get(
'https://api.github.com/repos/screenly/screenly-ose/git/refs/heads/{}'.format(branch)
)
resp.raise_for_status()
except exceptions.RequestException as exc:
handle_github_error(exc, 'remote branch HEAD')
return None, False
logging.debug('Got response from GitHub: {}'.format(resp.status_code))
latest_sha = resp.json()['object']['sha']
r.set('latest-remote-hash', latest_sha)
# Cache the result for the REMOTE_BRANCH_STATUS_TTL
r.expire('latest-remote-hash', REMOTE_BRANCH_STATUS_TTL)
return latest_sha, True
return get_cache, False
def is_up_to_date():
"""
Primitive update check. Checks local hash against GitHub hash for branch.
Returns True if the player is up to date.
"""
latest_sha, retrieved_update = fetch_remote_hash()
git_branch = get_git_branch()
git_hash = get_git_hash()
git_short_hash = get_git_short_hash()
get_device_id = r.get('device_id')
if not latest_sha:
logging.error('Unable to get latest version from GitHub')
return True
if not get_device_id:
device_id = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(15))
r.set('device_id', device_id)
else:
device_id = get_device_id
if retrieved_update:
if not settings['analytics_opt_out'] and not is_ci():
mp = Mixpanel('d18d9143e39ffdb2a4ee9dcc5ed16c56')
try:
mp.track(device_id, 'Version', {
'Branch': str(git_branch),
'Hash': str(git_short_hash),
'NOOBS': os.path.isfile('/boot/os_config.json'),
'Balena': is_balena_app(),
'Docker': is_docker(),
'Pi_Version': lookup_raspberry_pi_revision(parse_cpu_info()['revision'])['model']
})
except MixpanelException:
pass
except AttributeError:
pass
return latest_sha == git_hash