Skip to content

Commit 647712c

Browse files
khvn26rolodato
andauthored
feat: Switch existing task processor health checks to new liveness probe (#5161)
Co-authored-by: Rodrigo López Dato <rodrigo.lopezdato@flagsmith.com>
1 parent 9460dc7 commit 647712c

File tree

12 files changed

+130
-107
lines changed

12 files changed

+130
-107
lines changed

Dockerfile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
# Build a SaaS API image:
2020
# $ GH_TOKEN=$(gh auth token) docker build -t flagsmith-saas-api:dev --target saas-api \
2121
# --secret="id=sse_pgp_pkey,src=./sse_pgp_pkey.key"\
22-
# --secret="id=github_private_cloud_token,env=GH_TOKEN" .
22+
# --secret="id=github_private_cloud_token,env=GH_TOKEN" .
2323

2424
# Build a Private Cloud Unified image:
2525
# $ GH_TOKEN=$(gh auth token) docker build -t flagsmith-private-cloud:dev --target private-cloud-unified \
26-
# --secret="id=github_private_cloud_token,env=GH_TOKEN" .
26+
# --secret="id=github_private_cloud_token,env=GH_TOKEN" .
2727

2828
# Table of Contents
2929
# Stages are described as stage-name [dependencies]
@@ -90,7 +90,7 @@ ARG PYTHON_VERSION
9090
RUN apk add build-base linux-headers curl git \
9191
python-${PYTHON_VERSION} \
9292
python-${PYTHON_VERSION}-dev \
93-
py${PYTHON_VERSION}-pip
93+
py${PYTHON_VERSION}-pip
9494

9595
COPY api/pyproject.toml api/poetry.lock api/Makefile ./
9696
ENV POETRY_VIRTUALENVS_IN_PROJECT=true \
@@ -119,7 +119,7 @@ FROM wolfi-base AS api-runtime
119119

120120
# Install Python and make it available to venv entrypoints
121121
ARG PYTHON_VERSION
122-
RUN apk add python-${PYTHON_VERSION} && \
122+
RUN apk add curl python-${PYTHON_VERSION} && \
123123
mkdir /build/ && ln -s /usr/local/ /build/.venv
124124

125125
WORKDIR /app
@@ -139,6 +139,9 @@ ENTRYPOINT ["/app/scripts/run-docker.sh"]
139139

140140
CMD ["migrate-and-serve"]
141141

142+
HEALTHCHECK --interval=2s --timeout=2s --retries=3 --start-period=20s \
143+
CMD curl -f http://localhost:8000/health/liveness || exit 1
144+
142145
# * api-runtime-private [api-runtime]
143146
FROM api-runtime AS api-runtime-private
144147

api/app/settings/common.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,14 +986,17 @@
986986
# Used to control the size(number of identities) of the project that can be self migrated to edge
987987
MAX_SELF_MIGRATABLE_IDENTITIES = env.int("MAX_SELF_MIGRATABLE_IDENTITIES", 100000)
988988

989+
# RUN_BY_PROCESSOR is set by the task processor entrypoint
990+
TASK_PROCESSOR_MODE = env.bool("RUN_BY_PROCESSOR", False)
991+
989992
# Setting to allow asynchronous tasks to be run synchronously for testing purposes
990993
# or in a separate thread for self-hosted users
991994
TASK_RUN_METHOD = env.enum(
992995
"TASK_RUN_METHOD",
993996
type=TaskRunMethod,
994997
default=(
995998
TaskRunMethod.TASK_PROCESSOR.value
996-
if env.bool("RUN_BY_PROCESSOR", False)
999+
if TASK_PROCESSOR_MODE
9971000
else TaskRunMethod.SEPARATE_THREAD.value
9981001
),
9991002
)

api/app/urls.py

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,67 +10,72 @@
1010
from . import views
1111

1212
urlpatterns = [
13-
re_path(r"^api/v1/", include("api.urls.deprecated", namespace="api-deprecated")),
14-
re_path(r"^api/v1/", include("api.urls.v1", namespace="api-v1")),
15-
re_path(r"^api/v2/", include("api.urls.v2", namespace="api-v2")),
16-
re_path(r"^admin/", admin.site.urls),
1713
re_path(r"^health/liveness/?", views.version_info),
1814
re_path(r"^health/readiness/?", include("health_check.urls")),
19-
re_path(r"^health", include("health_check.urls", namespace="health")),
20-
# Aptible health checks must be on /healthcheck and cannot redirect
21-
# see https://www.aptible.com/docs/core-concepts/apps/connecting-to-apps/app-endpoints/https-endpoints/health-checks
22-
path("healthcheck", include("health_check.urls", namespace="aptible")),
23-
re_path(r"^version", views.version_info, name="version-info"),
24-
re_path(
25-
r"^sales-dashboard/",
26-
include("sales_dashboard.urls", namespace="sales_dashboard"),
27-
),
28-
# this url is used to generate email content for the password reset workflow
29-
re_path(
30-
r"^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,"
31-
r"13}-[0-9A-Za-z]{1,20})/$",
32-
password_reset_redirect,
33-
name="password_reset_confirm",
34-
),
35-
re_path(
36-
r"^config/project-overrides",
37-
views.project_overrides,
38-
name="project_overrides",
39-
),
4015
path("processor/", include("task_processor.urls")),
41-
path(
42-
"robots.txt",
43-
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
44-
),
4516
]
4617

47-
if settings.DEBUG:
48-
import debug_toolbar # type: ignore[import-untyped,unused-ignore]
18+
if not settings.TASK_PROCESSOR_MODE:
19+
urlpatterns += [
20+
re_path(
21+
r"^api/v1/", include("api.urls.deprecated", namespace="api-deprecated")
22+
),
23+
re_path(r"^api/v1/", include("api.urls.v1", namespace="api-v1")),
24+
re_path(r"^api/v2/", include("api.urls.v2", namespace="api-v2")),
25+
re_path(r"^admin/", admin.site.urls),
26+
re_path(r"^health", include("health_check.urls", namespace="health")),
27+
# Aptible health checks must be on /healthcheck and cannot redirect
28+
# see https://www.aptible.com/docs/core-concepts/apps/connecting-to-apps/app-endpoints/https-endpoints/health-checks
29+
path("healthcheck", include("health_check.urls", namespace="aptible")),
30+
re_path(r"^version", views.version_info, name="version-info"),
31+
re_path(
32+
r"^sales-dashboard/",
33+
include("sales_dashboard.urls", namespace="sales_dashboard"),
34+
),
35+
# this url is used to generate email content for the password reset workflow
36+
re_path(
37+
r"^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,"
38+
r"13}-[0-9A-Za-z]{1,20})/$",
39+
password_reset_redirect,
40+
name="password_reset_confirm",
41+
),
42+
re_path(
43+
r"^config/project-overrides",
44+
views.project_overrides,
45+
name="project_overrides",
46+
),
47+
path(
48+
"robots.txt",
49+
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
50+
),
51+
]
4952

53+
if settings.DEBUG:
5054
urlpatterns = [
51-
re_path(r"^__debug__/", include(debug_toolbar.urls)), # type: ignore[attr-defined,unused-ignore]
55+
re_path(r"^__debug__/", include("debug_toolbar.urls")),
5256
] + urlpatterns
5357

54-
if settings.SAML_INSTALLED:
55-
urlpatterns.append(path("api/v1/auth/saml/", include("saml.urls")))
58+
if settings.SAML_INSTALLED: # pragma: no cover
59+
urlpatterns += [
60+
path("api/v1/auth/saml/", include("saml.urls")),
61+
]
5662

5763
if settings.WORKFLOWS_LOGIC_INSTALLED: # pragma: no cover
5864
workflow_views = importlib.import_module("workflows_logic.views")
59-
urlpatterns.extend(
60-
[
61-
path("api/v1/features/workflows/", include("workflows_logic.urls")),
62-
path(
63-
"api/v1/environments/<str:environment_api_key>/create-change-request/",
64-
workflow_views.create_change_request,
65-
name="create-change-request",
66-
),
67-
path(
68-
"api/v1/environments/<str:environment_api_key>/list-change-requests/",
69-
workflow_views.list_change_requests,
70-
name="list-change-requests",
71-
),
72-
]
73-
)
65+
urlpatterns += [
66+
path("api/v1/features/workflows/", include("workflows_logic.urls")),
67+
path(
68+
"api/v1/environments/<str:environment_api_key>/create-change-request/",
69+
workflow_views.create_change_request,
70+
name="create-change-request",
71+
),
72+
path(
73+
"api/v1/environments/<str:environment_api_key>/list-change-requests/",
74+
workflow_views.list_change_requests,
75+
name="list-change-requests",
76+
),
77+
]
78+
7479

7580
if settings.SERVE_FE_ASSETS: # pragma: no cover
7681
# add route to serve FE assets for any unrecognised paths

api/manage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
import sys
44

55
if __name__ == "__main__":
6+
# Backwards compatibility for task-processor health checks
7+
# See https://github.com/Flagsmith/flagsmith-task-processor/issues/24
8+
if "checktaskprocessorthreadhealth" in sys.argv:
9+
import scripts.healthcheck
10+
11+
scripts.healthcheck.main()
12+
613
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings.local")
714

815
try:

api/poetry.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ exclude_also = [
6363
]
6464

6565
[tool.coverage.run]
66-
omit = ["scripts/*"]
66+
omit = ["scripts/*", "manage.py"]
6767

6868
[tool.pytest.ini_options]
6969
addopts = ['--ds=app.settings.test', '-vvvv', '-p', 'no:warnings']
@@ -146,7 +146,7 @@ pygithub = "2.1.1"
146146
hubspot-api-client = "^8.2.1"
147147
djangorestframework-dataclasses = "^1.3.1"
148148
pyotp = "^2.9.0"
149-
flagsmith-task-processor = { git = "https://github.com/Flagsmith/flagsmith-task-processor", tag = "v1.2.2" }
149+
flagsmith-task-processor = { git = "https://github.com/Flagsmith/flagsmith-task-processor", tag = "v1.3.1" }
150150
flagsmith-common = { git = "https://github.com/Flagsmith/flagsmith-common", tag = "v1.4.2" }
151151
tzdata = "^2024.1"
152152
djangorestframework-simplejwt = "^5.3.1"

api/scripts/__init__.py

Whitespace-only changes.

api/scripts/healthcheck.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1+
import logging
12
import sys
23

34
import requests
45

5-
url = "http://localhost:8000/health/liveness"
6-
status = requests.get(url).status_code
6+
HEALTH_LIVENESS_URL = "http://localhost:8000/health/liveness"
77

8-
sys.exit(0 if 200 >= status < 300 else 1)
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
def main() -> None:
13+
logger.warning(
14+
f"This healthcheck, invoked by {' '.join(sys.argv)}, is deprecated. "
15+
f"Use the `{HEALTH_LIVENESS_URL}` endpoint instead."
16+
)
17+
status_code = requests.get(HEALTH_LIVENESS_URL).status_code
18+
19+
if status_code != 200:
20+
logger.error(f"Health check failed with status {status_code}")
21+
22+
sys.exit(0 if 200 >= status_code < 300 else 1)
23+
24+
25+
if __name__ == "__main__":
26+
main()

api/scripts/run-docker.sh

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
#!/bin/sh
22
set -e
33

4+
# common environment variables
5+
ACCESS_LOG_FORMAT=${ACCESS_LOG_FORMAT:-'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %({origin}i)s %({access-control-allow-origin}o)s'}
6+
GUNICORN_LOGGER_CLASS=${GUNICORN_LOGGER_CLASS:-'util.logging.GunicornJsonCapableLogger'}
7+
48
waitfordb() {
59
if [ -z "${SKIP_WAIT_FOR_DB}" ]; then
610
python manage.py waitfordb "$@"
@@ -27,8 +31,8 @@ serve() {
2731
--workers ${GUNICORN_WORKERS:-3} \
2832
--threads ${GUNICORN_THREADS:-2} \
2933
--access-logfile $ACCESS_LOG_LOCATION \
30-
--logger-class ${GUNICORN_LOGGER_CLASS:-'util.logging.GunicornJsonCapableLogger'} \
31-
--access-logformat ${ACCESS_LOG_FORMAT:-'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %({origin}i)s %({access-control-allow-origin}o)s'} \
34+
--logger-class $GUNICORN_LOGGER_CLASS \
35+
--access-logformat "$ACCESS_LOG_FORMAT" \
3236
--keep-alive ${GUNICORN_KEEP_ALIVE:-2} \
3337
${STATSD_HOST:+--statsd-host $STATSD_HOST:$STATSD_PORT} \
3438
${STATSD_HOST:+--statsd-prefix $STATSD_PREFIX} \
@@ -39,11 +43,16 @@ run_task_processor() {
3943
if [ -n "$ANALYTICS_DATABASE_URL" ] || [ -n "$DJANGO_DB_NAME_ANALYTICS" ]; then
4044
waitfordb --waitfor 30 --migrations --database analytics
4145
fi
42-
RUN_BY_PROCESSOR=1 exec python manage.py runprocessor \
43-
--sleepintervalms ${TASK_PROCESSOR_SLEEP_INTERVAL:-500} \
44-
--graceperiodms ${TASK_PROCESSOR_GRACE_PERIOD_MS:-20000} \
45-
--numthreads ${TASK_PROCESSOR_NUM_THREADS:-5} \
46-
--queuepopsize ${TASK_PROCESSOR_QUEUE_POP_SIZE:-10}
46+
RUN_BY_PROCESSOR=1 python manage.py runprocessor \
47+
--sleepintervalms ${TASK_PROCESSOR_SLEEP_INTERVAL_MS:-${TASK_PROCESSOR_SLEEP_INTERVAL:-500}} \
48+
--graceperiodms ${TASK_PROCESSOR_GRACE_PERIOD_MS:-20000} \
49+
--numthreads ${TASK_PROCESSOR_NUM_THREADS:-5} \
50+
--queuepopsize ${TASK_PROCESSOR_QUEUE_POP_SIZE:-10} \
51+
gunicorn \
52+
--bind 0.0.0.0:8000 \
53+
--access-logfile $ACCESS_LOG_LOCATION \
54+
--logger-class $GUNICORN_LOGGER_CLASS \
55+
--access-logformat "$ACCESS_LOG_FORMAT"
4756
}
4857
migrate_analytics_db(){
4958
# if `$ANALYTICS_DATABASE_URL` or DJANGO_DB_NAME_ANALYTICS is set

docker-compose.pgpool.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,6 @@ services:
3434
TASK_RUN_METHOD: TASK_PROCESSOR # other options are: SYNCHRONOUSLY, SEPARATE_THREAD (default)
3535
ports:
3636
- 8000:8000
37-
healthcheck:
38-
test: ['CMD-SHELL', 'python /app/scripts/healthcheck.py']
39-
interval: 2s
40-
timeout: 2s
41-
retries: 20
42-
start_period: 20s
4337
depends_on:
4438
pgpool:
4539
condition: service_healthy

0 commit comments

Comments
 (0)