flagsmith-common 3.8.1__tar.gz → 3.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/PKG-INFO +2 -2
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/pyproject.toml +2 -2
- flagsmith_common-3.9.0/src/common/core/templates/docgen-events.md +9 -0
- flagsmith_common-3.9.0/src/common/core/templates/docgen-metrics.md +10 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/admin.py +12 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/exceptions.py +10 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/managers.py +5 -2
- flagsmith_common-3.9.0/src/task_processor/migrations/0015_add_is_disabled.py +37 -0
- flagsmith_common-3.9.0/src/task_processor/migrations/sql/0015_get_recurringtasks_to_process.sql +33 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/models.py +44 -1
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/processor.py +53 -37
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/threads.py +2 -2
- flagsmith_common-3.8.1/src/common/core/templates/docgen-events.md +0 -20
- flagsmith_common-3.8.1/src/common/core/templates/docgen-metrics.md +0 -24
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/LICENSE +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/README.md +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/app.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/cli/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/cli/healthcheck.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/constants.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/docgen/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/docgen/events.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/logging.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/main.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/docgen.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/start.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/waitfordb.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/metrics.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/middleware.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/otel.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/sentry.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/urls.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/utils.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/views.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/environments/permissions.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/multivariate/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/multivariate/serializers.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/serializers.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/versioning/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/versioning/serializers.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/conf.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/constants.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/logging.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/metrics.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/metrics_server.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/middleware.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/processors.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/gunicorn/utils.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/lint_tests.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/migrations/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/migrations/helpers/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/organisations/permissions.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/projects/permissions.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/prometheus/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/prometheus/utils.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/py.typed +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/test_tools/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/test_tools/plugin.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/test_tools/types.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/test_tools/utils.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/types.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/api.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/constants.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/dynamodb.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/py.typed +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/pydantic_types.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/types.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/utils.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/flagsmith_schemas/validators.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/apps.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/decorators.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/health.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/metrics.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0001_initial.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0014_add_trace_context.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/sql/__init__.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/monitoring.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/py.typed +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/routers.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/serializers.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/task_registry.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/task_run_method.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/tasks.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/types.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/urls.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/utils.py +0 -0
- {flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith-common
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.0
|
|
4
4
|
Summary: Flagsmith's common library
|
|
5
5
|
Author: Matthew Elwell, Gagan Trivedi, Kim Gustyr, Zach Aysan, Francesco Lo Franco, Rodrigo López Dato, Evandro Myller, Wadii Zaim
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -15,7 +15,7 @@ Requires-Dist: djangorestframework-recursive ; extra == 'common-core'
|
|
|
15
15
|
Requires-Dist: djangorestframework ; extra == 'common-core'
|
|
16
16
|
Requires-Dist: drf-spectacular>=0.28.0,<1 ; extra == 'common-core'
|
|
17
17
|
Requires-Dist: drf-writable-nested ; extra == 'common-core'
|
|
18
|
-
Requires-Dist: environs<
|
|
18
|
+
Requires-Dist: environs<16 ; extra == 'common-core'
|
|
19
19
|
Requires-Dist: gunicorn>=19.1 ; extra == 'common-core'
|
|
20
20
|
Requires-Dist: inflection ; extra == 'common-core'
|
|
21
21
|
Requires-Dist: opentelemetry-api>=1.25,<2 ; extra == 'common-core'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flagsmith-common"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.9.0"
|
|
4
4
|
description = "Flagsmith's common library"
|
|
5
5
|
requires-python = ">=3.11,<4.0"
|
|
6
6
|
dependencies = []
|
|
@@ -14,7 +14,7 @@ optional-dependencies = { test-tools = [
|
|
|
14
14
|
"djangorestframework",
|
|
15
15
|
"drf-spectacular (>=0.28.0,<1)",
|
|
16
16
|
"drf-writable-nested",
|
|
17
|
-
"environs (<
|
|
17
|
+
"environs (<16)",
|
|
18
18
|
"gunicorn (>=19.1)",
|
|
19
19
|
"inflection",
|
|
20
20
|
"opentelemetry-api (>=1.25,<2)",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{% for event in flagsmith_events %}
|
|
2
|
+
### `{{ event.name }}`
|
|
3
|
+
|
|
4
|
+
Logged at `{{ event.level }}` from:
|
|
5
|
+
{% for location in event.locations %} - `{{ location.path }}:{{ location.line }}`
|
|
6
|
+
{% endfor %}
|
|
7
|
+
Attributes:
|
|
8
|
+
{% for attr in event.attributes %} - `{{ attr }}`
|
|
9
|
+
{% endfor %}{% endfor %}
|
|
@@ -16,7 +16,11 @@ class RecurringTaskAdmin(admin.ModelAdmin[RecurringTask]):
|
|
|
16
16
|
"last_run_status",
|
|
17
17
|
"last_run_finished_at",
|
|
18
18
|
"is_locked",
|
|
19
|
+
"is_disabled",
|
|
20
|
+
"num_consecutive_failures",
|
|
19
21
|
)
|
|
22
|
+
list_filter = ("is_disabled",)
|
|
23
|
+
actions = ("unlock", "enable")
|
|
20
24
|
readonly_fields = ("args", "kwargs")
|
|
21
25
|
|
|
22
26
|
def last_run_status(self, instance: RecurringTask) -> str | None:
|
|
@@ -36,3 +40,11 @@ class RecurringTaskAdmin(admin.ModelAdmin[RecurringTask]):
|
|
|
36
40
|
queryset: QuerySet[RecurringTask],
|
|
37
41
|
) -> None:
|
|
38
42
|
queryset.update(is_locked=False)
|
|
43
|
+
|
|
44
|
+
@admin.action(description="Re-enable selected tasks")
|
|
45
|
+
def enable(
|
|
46
|
+
self,
|
|
47
|
+
request: HttpRequest,
|
|
48
|
+
queryset: QuerySet[RecurringTask],
|
|
49
|
+
) -> None:
|
|
50
|
+
queryset.update(is_disabled=False, num_consecutive_failures=0)
|
|
@@ -26,3 +26,13 @@ class TaskBackoffError(TaskProcessingError):
|
|
|
26
26
|
|
|
27
27
|
class TaskQueueFullError(Exception):
|
|
28
28
|
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TaskAbandonedError(TaskProcessingError):
|
|
32
|
+
"""
|
|
33
|
+
Marker error for recurring task runs whose worker died before
|
|
34
|
+
recording the result (process killed, OOM, host evicted, DB
|
|
35
|
+
connection lost during the post-execution save). Never raised —
|
|
36
|
+
used as the prefix in `error_details` so monitoring and log scrapers
|
|
37
|
+
can match on a single authoritative class name.
|
|
38
|
+
"""
|
|
@@ -14,5 +14,8 @@ class TaskManager(Manager["Task"]):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class RecurringTaskManager(Manager["RecurringTask"]):
|
|
17
|
-
def
|
|
18
|
-
return
|
|
17
|
+
def get_task_to_process(self) -> "RecurringTask | None":
|
|
18
|
+
return next(
|
|
19
|
+
iter(self.raw("SELECT * FROM get_recurringtasks_to_process()")),
|
|
20
|
+
None,
|
|
21
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
from common.migrations.helpers import PostgresOnlyRunSQL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
("task_processor", "0014_add_trace_context"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AddField(
|
|
16
|
+
model_name="recurringtask",
|
|
17
|
+
name="is_disabled",
|
|
18
|
+
field=models.BooleanField(default=False),
|
|
19
|
+
),
|
|
20
|
+
migrations.AddField(
|
|
21
|
+
model_name="recurringtask",
|
|
22
|
+
name="num_consecutive_failures",
|
|
23
|
+
field=models.IntegerField(default=0),
|
|
24
|
+
),
|
|
25
|
+
PostgresOnlyRunSQL.from_sql_file(
|
|
26
|
+
os.path.join(
|
|
27
|
+
os.path.dirname(__file__),
|
|
28
|
+
"sql",
|
|
29
|
+
"0015_get_recurringtasks_to_process.sql",
|
|
30
|
+
),
|
|
31
|
+
reverse_sql=os.path.join(
|
|
32
|
+
os.path.dirname(__file__),
|
|
33
|
+
"sql",
|
|
34
|
+
"0013_get_recurringtasks_to_process.sql",
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
]
|
flagsmith_common-3.9.0/src/task_processor/migrations/sql/0015_get_recurringtasks_to_process.sql
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION get_recurringtasks_to_process()
|
|
2
|
+
RETURNS SETOF task_processor_recurringtask AS $$
|
|
3
|
+
DECLARE
|
|
4
|
+
row_to_return task_processor_recurringtask;
|
|
5
|
+
BEGIN
|
|
6
|
+
-- Select the tasks that needs to be processed
|
|
7
|
+
FOR row_to_return IN
|
|
8
|
+
SELECT *
|
|
9
|
+
FROM task_processor_recurringtask
|
|
10
|
+
-- Skip disabled tasks; add one minute to the timeout as a grace period for overhead
|
|
11
|
+
WHERE is_disabled = FALSE
|
|
12
|
+
AND (is_locked = FALSE OR (locked_at IS NOT NULL AND locked_at < NOW() - timeout + INTERVAL '1 minute'))
|
|
13
|
+
ORDER BY last_picked_at NULLS FIRST
|
|
14
|
+
LIMIT 1
|
|
15
|
+
-- Select for update to ensure that no other workers can select these tasks while in this transaction block
|
|
16
|
+
FOR UPDATE SKIP LOCKED
|
|
17
|
+
LOOP
|
|
18
|
+
-- Lock every selected task(by updating `is_locked` to true)
|
|
19
|
+
UPDATE task_processor_recurringtask
|
|
20
|
+
-- Lock this row by setting is_locked True, so that no other workers can select these tasks after this
|
|
21
|
+
-- transaction is complete (but the tasks are still being executed by the current worker)
|
|
22
|
+
SET is_locked = TRUE, locked_at = NOW(), last_picked_at = NOW()
|
|
23
|
+
WHERE id = row_to_return.id;
|
|
24
|
+
-- If we don't explicitly update the columns here, the client will receive a row
|
|
25
|
+
-- that is locked but still shows `is_locked` as `False` and `locked_at` as `None`.
|
|
26
|
+
row_to_return.is_locked := TRUE;
|
|
27
|
+
row_to_return.locked_at := NOW();
|
|
28
|
+
RETURN NEXT row_to_return;
|
|
29
|
+
END LOOP;
|
|
30
|
+
|
|
31
|
+
RETURN;
|
|
32
|
+
END;
|
|
33
|
+
$$ LANGUAGE plpgsql
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import typing
|
|
2
3
|
import uuid
|
|
3
4
|
from datetime import datetime, timedelta
|
|
@@ -7,11 +8,13 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|
|
7
8
|
from django.db import models
|
|
8
9
|
from django.utils import timezone
|
|
9
10
|
|
|
10
|
-
from task_processor.exceptions import TaskQueueFullError
|
|
11
|
+
from task_processor.exceptions import TaskAbandonedError, TaskQueueFullError
|
|
11
12
|
from task_processor.managers import RecurringTaskManager, TaskManager
|
|
12
13
|
from task_processor.task_registry import get_task, registered_tasks
|
|
13
14
|
from task_processor.types import TaskCallable, TraceContext
|
|
14
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
15
18
|
_django_json_encoder_default = DjangoJSONEncoder().default
|
|
16
19
|
|
|
17
20
|
|
|
@@ -151,6 +154,8 @@ class Task(AbstractBaseTask):
|
|
|
151
154
|
|
|
152
155
|
|
|
153
156
|
class RecurringTask(AbstractBaseTask):
|
|
157
|
+
MAX_CONSECUTIVE_FAILURES = 4
|
|
158
|
+
|
|
154
159
|
run_every = models.DurationField()
|
|
155
160
|
first_run_time = models.TimeField(blank=True, null=True)
|
|
156
161
|
|
|
@@ -158,6 +163,8 @@ class RecurringTask(AbstractBaseTask):
|
|
|
158
163
|
timeout = models.DurationField(default=timedelta(minutes=30))
|
|
159
164
|
|
|
160
165
|
last_picked_at = models.DateTimeField(blank=True, null=True)
|
|
166
|
+
is_disabled = models.BooleanField(default=False)
|
|
167
|
+
num_consecutive_failures = models.IntegerField(default=0)
|
|
161
168
|
objects: RecurringTaskManager = RecurringTaskManager()
|
|
162
169
|
|
|
163
170
|
class Meta:
|
|
@@ -172,6 +179,42 @@ class RecurringTask(AbstractBaseTask):
|
|
|
172
179
|
self.is_locked = False
|
|
173
180
|
self.locked_at = None
|
|
174
181
|
|
|
182
|
+
def reconcile_abandoned_run(self) -> None:
|
|
183
|
+
# if for some reason the worker died before before writing the task run result
|
|
184
|
+
# we mark that run as explict failure here
|
|
185
|
+
abandoned_run = self.task_runs.filter(result__isnull=True).first()
|
|
186
|
+
if abandoned_run is None:
|
|
187
|
+
return
|
|
188
|
+
abandoned_run.finished_at = timezone.now()
|
|
189
|
+
abandoned_run.result = TaskResult.FAILURE.value
|
|
190
|
+
abandoned_run.error_details = (
|
|
191
|
+
f"{TaskAbandonedError.__name__}: "
|
|
192
|
+
"no result was written before the SQL reaper unlocked the task"
|
|
193
|
+
)
|
|
194
|
+
abandoned_run.save(
|
|
195
|
+
update_fields=["finished_at", "result", "error_details"],
|
|
196
|
+
)
|
|
197
|
+
logger.error(
|
|
198
|
+
"Recurring task '%s' was abandoned: %s",
|
|
199
|
+
self.task_identifier,
|
|
200
|
+
abandoned_run.error_details,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def mark_failure(self) -> None:
|
|
204
|
+
super().mark_failure()
|
|
205
|
+
self.num_consecutive_failures += 1
|
|
206
|
+
if self.num_consecutive_failures >= self.MAX_CONSECUTIVE_FAILURES:
|
|
207
|
+
self.is_disabled = True
|
|
208
|
+
logger.error(
|
|
209
|
+
"Recurring task '%s' auto-disabled after %d consecutive failures",
|
|
210
|
+
self.task_identifier,
|
|
211
|
+
self.num_consecutive_failures,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def mark_success(self) -> None:
|
|
215
|
+
super().mark_success()
|
|
216
|
+
self.num_consecutive_failures = 0
|
|
217
|
+
|
|
175
218
|
@property
|
|
176
219
|
def should_execute(self) -> bool:
|
|
177
220
|
now = timezone.now()
|
|
@@ -7,6 +7,7 @@ from datetime import timedelta
|
|
|
7
7
|
from importlib.metadata import version
|
|
8
8
|
|
|
9
9
|
from django.conf import settings
|
|
10
|
+
from django.db import close_old_connections
|
|
10
11
|
from django.utils import timezone
|
|
11
12
|
from opentelemetry import context as otel_context
|
|
12
13
|
from opentelemetry import propagate, trace
|
|
@@ -68,52 +69,66 @@ def run_tasks(database: str, num_tasks: int = 1) -> list[TaskRun]:
|
|
|
68
69
|
return []
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
def
|
|
72
|
+
def run_recurring_task(database: str) -> RecurringTaskRun | None:
|
|
72
73
|
# NOTE: We will probably see a lot of delay in the execution of recurring tasks
|
|
73
74
|
# if the tasks take longer then `run_every` to execute. This is not
|
|
74
75
|
# a problem for now, but we should be mindful of this limitation
|
|
75
76
|
task_manager: RecurringTaskManager = RecurringTask.objects.db_manager(database)
|
|
76
|
-
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
77
|
+
task = task_manager.get_task_to_process()
|
|
78
|
+
if task is None:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
logger.debug(f"Running recurring task '{task.task_identifier}'")
|
|
82
|
+
|
|
83
|
+
task.reconcile_abandoned_run()
|
|
84
|
+
|
|
85
|
+
if not task.is_task_registered:
|
|
86
|
+
# This is necessary to ensure that old instances of the task processor,
|
|
87
|
+
# which may still be running during deployment, do not remove tasks added by new instances.
|
|
88
|
+
# Reference: https://github.com/Flagsmith/flagsmith/issues/2551
|
|
89
|
+
task_age = timezone.now() - task.created_at
|
|
90
|
+
if task_age > UNREGISTERED_RECURRING_TASK_GRACE_PERIOD:
|
|
91
|
+
task.delete(using=database)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
task_run: RecurringTaskRun | None = None
|
|
95
|
+
if task.should_execute:
|
|
96
|
+
# Persist the task run before execution so that, if the worker is
|
|
97
|
+
# killed mid-task, we still have a row we can later mark as timed
|
|
98
|
+
# out when the task is unlocked by the timeout-based reaper in
|
|
99
|
+
# `get_recurringtasks_to_process`.
|
|
100
|
+
task_run = RecurringTaskRun(started_at=timezone.now(), task=task)
|
|
101
|
+
task_run.save(using=database)
|
|
102
|
+
task, run = _run_task(task, task_run=task_run)
|
|
103
|
+
assert run is task_run
|
|
104
|
+
# task.run() may have idled the DB connection past the server's
|
|
105
|
+
# session timeout; drop stale connections so the saves below open
|
|
106
|
+
# a fresh one. See Sentry FLAGSMITH-API-5EM.
|
|
107
|
+
close_old_connections()
|
|
108
|
+
else:
|
|
109
|
+
task.unlock()
|
|
110
|
+
|
|
111
|
+
task.save(
|
|
112
|
+
using=database,
|
|
113
|
+
update_fields=[
|
|
114
|
+
"is_locked",
|
|
115
|
+
"locked_at",
|
|
116
|
+
"is_disabled",
|
|
117
|
+
"num_consecutive_failures",
|
|
118
|
+
],
|
|
119
|
+
)
|
|
105
120
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
if task_run:
|
|
122
|
+
task_run.save(using=database)
|
|
123
|
+
logger.debug(f"Finished running recurring task '{task.task_identifier}'")
|
|
124
|
+
return task_run
|
|
109
125
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return []
|
|
126
|
+
return None
|
|
113
127
|
|
|
114
128
|
|
|
115
129
|
def _run_task(
|
|
116
130
|
task: T,
|
|
131
|
+
task_run: AnyTaskRun | None = None,
|
|
117
132
|
) -> typing.Tuple[T, AnyTaskRun]:
|
|
118
133
|
assert settings.TASK_PROCESSOR_MODE, (
|
|
119
134
|
"Attempt to run tasks in a non-task-processor environment"
|
|
@@ -129,7 +144,8 @@ def _run_task(
|
|
|
129
144
|
logger.debug(
|
|
130
145
|
f"Running task {task_identifier} id={task.pk} args={task.args} kwargs={task.kwargs}"
|
|
131
146
|
)
|
|
132
|
-
task_run
|
|
147
|
+
if task_run is None:
|
|
148
|
+
task_run = task.task_runs.model(started_at=timezone.now(), task=task) # type: ignore[attr-defined]
|
|
133
149
|
result: str
|
|
134
150
|
executor = None
|
|
135
151
|
|
|
@@ -8,7 +8,7 @@ from django.conf import settings
|
|
|
8
8
|
from django.db import close_old_connections
|
|
9
9
|
from django.utils import timezone
|
|
10
10
|
|
|
11
|
-
from task_processor.processor import
|
|
11
|
+
from task_processor.processor import run_recurring_task, run_tasks
|
|
12
12
|
from task_processor.task_registry import initialise
|
|
13
13
|
from task_processor.types import TaskProcessorConfig
|
|
14
14
|
|
|
@@ -111,7 +111,7 @@ class TaskRunner(Thread):
|
|
|
111
111
|
|
|
112
112
|
# Recurring tasks are only run on one database
|
|
113
113
|
if (database == "default") ^ database_is_separate:
|
|
114
|
-
|
|
114
|
+
run_recurring_task(database)
|
|
115
115
|
except Exception as exception:
|
|
116
116
|
# To prevent task threads from dying if they get an error retrieving the tasks from the
|
|
117
117
|
# database this will allow the thread to continue trying to retrieve tasks if it can
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Events
|
|
3
|
-
sidebar_label: Events
|
|
4
|
-
sidebar_position: 30
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Flagsmith backend emits [OpenTelemetry events](https://opentelemetry.io/docs/specs/otel/logs/data-model/#events)
|
|
8
|
-
that can be ingested to downstream observability systems and/or a data warehouse of your choice via OTLP.
|
|
9
|
-
To learn how to configure this, see [OpenTelemetry](deployment-self-hosting/scaling-and-performance/opentelemetry).
|
|
10
|
-
|
|
11
|
-
## Event catalogue
|
|
12
|
-
{% for event in flagsmith_events %}
|
|
13
|
-
### `{{ event.name }}`
|
|
14
|
-
|
|
15
|
-
Logged at `{{ event.level }}` from:
|
|
16
|
-
{% for location in event.locations %} - `{{ location.path }}:{{ location.line }}`
|
|
17
|
-
{% endfor %}
|
|
18
|
-
Attributes:
|
|
19
|
-
{% for attr in event.attributes %} - `{{ attr }}`
|
|
20
|
-
{% endfor %}{% endfor %}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Metrics
|
|
3
|
-
sidebar_label: Metrics
|
|
4
|
-
sidebar_position: 20
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Prometheus
|
|
8
|
-
|
|
9
|
-
To enable the Prometheus `/metrics` endpoint, set the `PROMETHEUS_ENABLED` environment variable to `true`.
|
|
10
|
-
|
|
11
|
-
When enabled, Flagsmith serves the `/metrics` endpoint on port 9100.
|
|
12
|
-
|
|
13
|
-
The metrics provided by Flagsmith are described below.
|
|
14
|
-
|
|
15
|
-
{% for metric in flagsmith_metrics %}
|
|
16
|
-
### `{{ metric.name }}`
|
|
17
|
-
|
|
18
|
-
{{ metric.type|title }}.
|
|
19
|
-
|
|
20
|
-
{{ metric.documentation }}
|
|
21
|
-
|
|
22
|
-
Labels:
|
|
23
|
-
{% for label in metric.labels %} - `{{ label }}`
|
|
24
|
-
{% endfor %}{% endfor %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/docgen.py
RENAMED
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/start.py
RENAMED
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/core/management/commands/waitfordb.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/multivariate/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/multivariate/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/versioning/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/features/versioning/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/common/migrations/helpers/postgres_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.8.1 → flagsmith_common-3.9.0}/src/task_processor/migrations/sql/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|