Make healthcheck a required parameter

In process_emails_from_sqs, require that a path to the healthcheck file
is set. For local development, place this at tmp/healthcheck.json, in a
new temporary directory for local development files.
This commit is contained in:
John Whitlock 2022-05-04 11:37:27 -05:00
Родитель e5f2ce7bd0
Коммит 73a146c8a7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 082C735D154FB750
8 изменённых файлов: 61 добавлений и 65 удалений

Просмотреть файл

@ -14,6 +14,7 @@ static/scss/libs
staticfiles
coverage/
junit.xml
tmp/
# frontend/.gitignore
frontend/public/mockServiceWorker.js

Просмотреть файл

@ -13,6 +13,7 @@ https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-read
from argparse import FileType
from datetime import datetime, timezone
import io
import json
import logging
@ -33,7 +34,7 @@ class Command(BaseCommand):
def add_arguments(self, parser):
"""Add command-line arguments (called by BaseCommand)"""
parser.add_argument(
"healthcheck_path",
"--healthcheck_path",
type=FileType("r", encoding="utf8"),
default=settings.PROCESS_EMAIL_HEALTHCHECK_PATH,
help=self.help_message_for_setting(
@ -52,7 +53,11 @@ class Command(BaseCommand):
def handle(self, healthcheck_path, max_age, verbosity, *args, **kwargs):
"""Handle call from command line (called by BaseCommand)"""
context = self.check_healthcheck(healthcheck_path, max_age)
if isinstance(healthcheck_path, io.TextIOWrapper):
healthcheck_file = healthcheck_path
else:
healthcheck_file = open(healthcheck_path, mode="r", encoding="utf8")
context = self.check_healthcheck(healthcheck_file, max_age)
if context["success"]:
if verbosity > 1:
logger.info("Healthcheck passed", extra=context)

Просмотреть файл

@ -89,9 +89,8 @@ class Command(BaseCommand):
)
healthcheck_path = healthcheck_path or settings.PROCESS_EMAIL_HEALTHCHECK_PATH
if healthcheck_path is None:
self.healthcheck_file = None
elif isinstance(healthcheck_path, io.TextIOWrapper):
assert healthcheck_path
if isinstance(healthcheck_path, io.TextIOWrapper):
self.healthcheck_file = healthcheck_path
else:
self.healthcheck_file = open(healthcheck_path, mode="w", encoding="utf8")
@ -123,9 +122,7 @@ class Command(BaseCommand):
assert self.wait_seconds > 0
assert self.visibility_seconds > 0
assert self.max_seconds is None or self.max_seconds > 0.0
assert self.healthcheck_file is None or isinstance(
self.healthcheck_file, io.TextIOWrapper
)
assert isinstance(self.healthcheck_file, io.TextIOWrapper)
assert 0 <= self.verbosity <= 3
def help_message_for_setting(self, text, setting_name):
@ -201,14 +198,13 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
"""Handle call from command line (called by BaseCommand)"""
self.init_vars(*args, **kwargs)
healthcheck_path = self.healthcheck_file.name if self.healthcheck_file else None
logger.info(
"Starting process_emails_from_sqs",
extra={
"batch_size": self.batch_size,
"wait_seconds": self.wait_seconds,
"visibility_seconds": self.visibility_seconds,
"healthcheck_path": healthcheck_path,
"healthcheck_path": self.healthcheck_file.name,
"delete_failed_messages": self.delete_failed_messages,
"max_seconds": self.max_seconds,
"aws_region": self.aws_region,
@ -224,6 +220,7 @@ class Command(BaseCommand):
process_data = self.process_queue()
logger.info("Exiting process_emails_from_sqs", extra=process_data)
self.healthcheck_file.close()
def create_client(self):
"""Create the SQS client."""
@ -504,8 +501,6 @@ class Command(BaseCommand):
def write_healthcheck(self):
"""Update the healthcheck file with operations data, if path is set."""
if not self.healthcheck_file:
return
data = {
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
"cycles": self.cycles,
@ -530,9 +525,3 @@ class Command(BaseCommand):
return f"{value} {singular}"
else:
return f"{value} {plural or (singular + 's')}"
def close_healthcheck(self):
"""Close the healthcheck file if open."""
if self.healthcheck_file:
self.healthcheck_file.close()
self.healthcheck_file = None

Просмотреть файл

@ -10,7 +10,15 @@ from django.core.management import call_command, CommandError
from emails.management.commands.check_health import Command
def write_healthcheck(folder, age=0):
@pytest.fixture(autouse=True)
def test_settings(settings, tmp_path):
"""Override settings for tests"""
settings.PROCESS_EMAIL_HEALTHCHECK_PATH = tmp_path / "healthcheck.json"
settings.PROCESS_EMAIL_MAX_AGE = 120
return settings
def write_healthcheck(path, age=0):
"""
Write a valid healthcheck file.
@ -20,45 +28,47 @@ def write_healthcheck(folder, age=0):
Returns the path to the healthcheck file
"""
path = folder / "healthcheck.json"
timestamp = (datetime.now(tz=timezone.utc) - timedelta(seconds=age)).isoformat()
data = {"timestamp": timestamp, "testing": True}
with path.open("w", encoding="utf8") as f:
json.dump(data, f)
return path
def test_check_health_passed_no_logs(tmp_path, caplog):
def test_check_health_passed_no_logs(test_settings, caplog):
"""check health succeeds when the timestamp is recent."""
path = write_healthcheck(tmp_path)
call_command("check_health", str(path))
path = test_settings.PROCESS_EMAIL_HEALTHCHECK_PATH
write_healthcheck(path)
call_command("check_health")
assert caplog.record_tuples == []
def test_check_health_passed_logs(tmp_path, caplog):
def test_check_health_passed_logs(test_settings, caplog):
"""check health success and logs at verbosity 2."""
path = write_healthcheck(tmp_path)
call_command("check_health", str(path), f"--verbosity=2")
path = test_settings.PROCESS_EMAIL_HEALTHCHECK_PATH
write_healthcheck(path)
call_command("check_health", "--verbosity=2")
assert caplog.record_tuples == [
("eventsinfo.check_health", logging.INFO, "Healthcheck passed")
]
def test_check_health_too_old(tmp_path, caplog):
def test_check_health_too_old(test_settings, caplog):
"""check health fails when the timestamp is too old."""
path = write_healthcheck(tmp_path, 130)
path = test_settings.PROCESS_EMAIL_HEALTHCHECK_PATH
write_healthcheck(path, 130)
with pytest.raises(CommandError) as excinfo:
call_command("check_health", str(path), "--max-age=120")
call_command("check_health")
assert str(excinfo.value) == "Healthcheck failed: Timestamp is too old"
assert caplog.record_tuples == [
("eventsinfo.check_health", logging.ERROR, "Healthcheck failed")
]
def test_check_health_failed_no_logs(tmp_path, caplog):
def test_check_health_failed_no_logs(test_settings, caplog):
"""check health failure do not log at verbosity=0."""
path = write_healthcheck(tmp_path, 130)
path = test_settings.PROCESS_EMAIL_HEALTHCHECK_PATH
write_healthcheck(path, 130)
with pytest.raises(CommandError) as excinfo:
call_command("check_health", str(path), "--max-age=120", "--verbosity=0")
call_command("check_health", "--verbosity=0")
assert str(excinfo.value) == "Healthcheck failed: Timestamp is too old"
assert caplog.record_tuples == []

Просмотреть файл

@ -60,7 +60,7 @@ def mock_sns_inbound_logic():
@pytest.fixture(autouse=True)
def test_settings(settings):
def test_settings(settings, tmp_path):
settings.AWS_SNS_TOPIC = {TEST_SNS_MESSAGE["TopicArn"]}
settings.AWS_REGION = "us-east-1"
@ -70,7 +70,7 @@ def test_settings(settings):
)
settings.PROCESS_EMAIL_BATCH_SIZE = 10
settings.PROCESS_EMAIL_DELETE_FAILED_MESSAGES = False
settings.PROCESS_EMAIL_HEALTHCHECK_PATH = None
settings.PROCESS_EMAIL_HEALTHCHECK_PATH = tmp_path / "healthcheck.json"
settings.PROCESS_EMAIL_MAX_SECONDS = None
settings.PROCESS_EMAIL_VERBOSITY = 2
settings.PROCESS_EMAIL_VISIBILITY_SECONDS = 120
@ -268,13 +268,9 @@ def test_process_queue_verify_sns_header_fails(test_settings):
assert res["failed_messages"] == 1
def test_process_queue_write_healthcheck(tmp_path):
"""write_healthcheck writes the timestamp to the specified path."""
healthcheck_path = tmp_path / "healthcheck.json"
with open(healthcheck_path, "w", encoding="utf8") as hc_file:
Command(
queue=fake_queue(), healthcheck_file=hc_file, max_seconds=3
).process_queue()
def test_process_queue_write_healthcheck(test_settings):
healthcheck_path = test_settings.PROCESS_EMAIL_HEALTHCHECK_PATH
Command(queue=fake_queue(), max_seconds=3).process_queue()
content = json.loads(healthcheck_path.read_bytes())
ts = datetime.fromisoformat(content["timestamp"])
duration = (datetime.now(tz=timezone.utc) - ts).total_seconds()
@ -335,14 +331,12 @@ def test_command_setup_from_settings():
assert command.verbosity == 2
assert command.visibility_seconds == 1200
assert command.wait_seconds == 10
command.close_healthcheck()
def test_write_healthcheck(tmp_path):
def test_write_healthcheck(test_settings):
"""write_healthcheck writes the timestamp to the specified path."""
healthcheck_path = tmp_path / "healthcheck.json"
with open(healthcheck_path, "w", encoding="utf8") as hc_file:
Command(queue=fake_queue(), healthcheck_file=hc_file).write_healthcheck()
healthcheck_path = test_settings.PROCESS_EMAIL_HEALTHCHECK_PATH
Command(queue=fake_queue()).write_healthcheck()
content = json.loads(healthcheck_path.read_bytes())
assert content == {
"timestamp": content["timestamp"],
@ -359,22 +353,11 @@ def test_write_healthcheck(tmp_path):
assert 0.0 < duration < 0.5
def test_write_healthcheck_twice(tmp_path):
def test_write_healthcheck_twice(test_settings):
"""write_healthcheck overwrites the file each time."""
healthcheck_path = tmp_path / "healthcheck.json"
with open(healthcheck_path, "w", encoding="utf8") as hc_file:
cmd = Command(queue=fake_queue(), healthcheck_file=hc_file)
cmd.write_healthcheck()
cmd.write_healthcheck()
healthcheck_path = test_settings.PROCESS_EMAIL_HEALTHCHECK_PATH
cmd = Command(queue=fake_queue())
cmd.write_healthcheck()
cmd.write_healthcheck()
content = json.loads(healthcheck_path.read_bytes())
assert content['queue_count_not_visible'] == 3
def test_close_healthcheck(tmp_path):
"""close_healthcheck() can be used to manually close the healthcheck file."""
healthcheck_path = tmp_path / "healthcheck.json"
command = Command(healthcheck_file=healthcheck_path)
assert command.healthcheck_file.name == str(healthcheck_path)
command.close_healthcheck()
assert command.healthcheck_file is None
command.close_healthcheck() # Closing twice is OK

Просмотреть файл

@ -39,7 +39,7 @@ except ImportError:
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TMP_DIR = os.path.join(BASE_DIR, "tmp")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
@ -718,7 +718,9 @@ PROCESS_EMAIL_BATCH_SIZE = config(
PROCESS_EMAIL_DELETE_FAILED_MESSAGES = config(
"PROCESS_EMAIL_DELETE_FAILED_MESSAGES", False, cast=bool
)
PROCESS_EMAIL_HEALTHCHECK_PATH = config("PROCESS_EMAIL_HEALTHCHECK_PATH", "") or None
PROCESS_EMAIL_HEALTHCHECK_PATH = config(
"PROCESS_EMAIL_HEALTHCHECK_PATH", os.path.join(TMP_DIR, "healthcheck.json")
)
PROCESS_EMAIL_MAX_SECONDS = config("PROCESS_EMAIL_MAX_SECONDS", 0, cast=int) or None
PROCESS_EMAIL_VERBOSITY = config(
"PROCESS_EMAIL_VERBOSITY", 1, cast=Choices(range(0, 4), cast=int)

3
tmp/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
*
!README.md
!.gitignore

3
tmp/README.md Normal file
Просмотреть файл

@ -0,0 +1,3 @@
# Temporary folder
This folder contains temporary files created in local development environments.