flagsmith-common 2.2.4__py3-none-any.whl
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.
- common/__init__.py +0 -0
- common/core/__init__.py +6 -0
- common/core/app.py +6 -0
- common/core/cli/__init__.py +0 -0
- common/core/cli/healthcheck.py +120 -0
- common/core/logging.py +24 -0
- common/core/main.py +105 -0
- common/core/management/__init__.py +0 -0
- common/core/management/commands/__init__.py +0 -0
- common/core/management/commands/docgen.py +63 -0
- common/core/management/commands/start.py +61 -0
- common/core/management/commands/waitfordb.py +87 -0
- common/core/metrics.py +25 -0
- common/core/middleware.py +22 -0
- common/core/templates/docgen-metrics.md +22 -0
- common/core/urls.py +17 -0
- common/core/utils.py +239 -0
- common/core/views.py +27 -0
- common/environments/permissions.py +15 -0
- common/features/__init__.py +0 -0
- common/features/multivariate/__init__.py +0 -0
- common/features/multivariate/serializers.py +19 -0
- common/features/serializers.py +68 -0
- common/features/versioning/__init__.py +0 -0
- common/features/versioning/serializers.py +13 -0
- common/gunicorn/__init__.py +0 -0
- common/gunicorn/conf.py +18 -0
- common/gunicorn/constants.py +23 -0
- common/gunicorn/logging.py +120 -0
- common/gunicorn/metrics.py +26 -0
- common/gunicorn/middleware.py +30 -0
- common/gunicorn/utils.py +104 -0
- common/migrations/__init__.py +0 -0
- common/migrations/helpers/__init__.py +9 -0
- common/migrations/helpers/postgres_helpers.py +41 -0
- common/organisations/permissions.py +10 -0
- common/projects/permissions.py +40 -0
- common/prometheus/__init__.py +3 -0
- common/prometheus/utils.py +38 -0
- common/py.typed +0 -0
- common/test_tools/__init__.py +11 -0
- common/test_tools/plugin.py +139 -0
- common/test_tools/types.py +56 -0
- common/test_tools/utils.py +11 -0
- common/types.py +45 -0
- flagsmith_common-2.2.4.dist-info/METADATA +196 -0
- flagsmith_common-2.2.4.dist-info/RECORD +92 -0
- flagsmith_common-2.2.4.dist-info/WHEEL +4 -0
- flagsmith_common-2.2.4.dist-info/entry_points.txt +6 -0
- flagsmith_common-2.2.4.dist-info/licenses/LICENSE +28 -0
- task_processor/__init__.py +0 -0
- task_processor/admin.py +38 -0
- task_processor/apps.py +47 -0
- task_processor/decorators.py +209 -0
- task_processor/exceptions.py +28 -0
- task_processor/health.py +44 -0
- task_processor/managers.py +18 -0
- task_processor/metrics.py +22 -0
- task_processor/migrations/0001_initial.py +44 -0
- task_processor/migrations/0002_healthcheckmodel.py +21 -0
- task_processor/migrations/0003_add_completed_to_task.py +22 -0
- task_processor/migrations/0004_recreate_task_indexes.py +43 -0
- task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
- task_processor/migrations/0006_auto_20230221_0802.py +45 -0
- task_processor/migrations/0007_add_is_locked.py +23 -0
- task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
- task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
- task_processor/migrations/0010_task_priority.py +27 -0
- task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
- task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
- task_processor/migrations/0013_add_last_picked_at.py +34 -0
- task_processor/migrations/__init__.py +0 -0
- task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
- task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
- task_processor/migrations/sql/__init__.py +0 -0
- task_processor/models.py +237 -0
- task_processor/monitoring.py +12 -0
- task_processor/processor.py +202 -0
- task_processor/py.typed +0 -0
- task_processor/routers.py +55 -0
- task_processor/serializers.py +7 -0
- task_processor/task_registry.py +90 -0
- task_processor/task_run_method.py +7 -0
- task_processor/tasks.py +71 -0
- task_processor/threads.py +128 -0
- task_processor/types.py +18 -0
- task_processor/urls.py +5 -0
- task_processor/utils.py +71 -0
- task_processor/views.py +20 -0
task_processor/health.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import backoff
|
|
4
|
+
from health_check.backends import BaseHealthCheckBackend # type: ignore[import-untyped]
|
|
5
|
+
from health_check.exceptions import HealthCheckException # type: ignore[import-untyped]
|
|
6
|
+
|
|
7
|
+
from task_processor.models import HealthCheckModel
|
|
8
|
+
from task_processor.tasks import create_health_check_model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_processor_healthy(max_tries: int = 5, factor: float = 0.1) -> bool:
|
|
12
|
+
health_check_model_uuid = str(uuid.uuid4())
|
|
13
|
+
|
|
14
|
+
create_health_check_model.delay(args=(health_check_model_uuid,))
|
|
15
|
+
|
|
16
|
+
@backoff.on_predicate(
|
|
17
|
+
backoff.expo,
|
|
18
|
+
lambda m: m is None,
|
|
19
|
+
max_tries=max_tries,
|
|
20
|
+
factor=factor,
|
|
21
|
+
jitter=None,
|
|
22
|
+
)
|
|
23
|
+
def get_health_check_model() -> HealthCheckModel | None:
|
|
24
|
+
return HealthCheckModel.objects.filter(uuid=health_check_model_uuid).first()
|
|
25
|
+
|
|
26
|
+
health_check_model = get_health_check_model()
|
|
27
|
+
if health_check_model:
|
|
28
|
+
health_check_model.delete()
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TaskProcessorHealthCheckBackend(BaseHealthCheckBackend): # type: ignore[misc]
|
|
35
|
+
#: The status endpoints will respond with a 200 status code
|
|
36
|
+
#: even if the check errors.
|
|
37
|
+
critical_service = False
|
|
38
|
+
|
|
39
|
+
def check_status(self) -> None:
|
|
40
|
+
if not is_processor_healthy():
|
|
41
|
+
raise HealthCheckException("Task processor is unable to process tasks.")
|
|
42
|
+
|
|
43
|
+
def identifier(self) -> str:
|
|
44
|
+
return self.__class__.__name__ # Display name on the endpoint.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from django.db.models import Manager
|
|
4
|
+
|
|
5
|
+
if typing.TYPE_CHECKING:
|
|
6
|
+
from django.db.models.query import RawQuerySet
|
|
7
|
+
|
|
8
|
+
from task_processor.models import RecurringTask, Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskManager(Manager["Task"]):
|
|
12
|
+
def get_tasks_to_process(self, num_tasks: int) -> "RawQuerySet[Task]":
|
|
13
|
+
return self.raw("SELECT * FROM get_tasks_to_process(%s)", [num_tasks])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RecurringTaskManager(Manager["RecurringTask"]):
|
|
17
|
+
def get_tasks_to_process(self) -> "RawQuerySet[RecurringTask]":
|
|
18
|
+
return self.raw("SELECT * FROM get_recurringtasks_to_process()")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import prometheus_client
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
|
|
4
|
+
from common.prometheus import Histogram
|
|
5
|
+
|
|
6
|
+
flagsmith_task_processor_enqueued_tasks_total = prometheus_client.Counter(
|
|
7
|
+
"flagsmith_task_processor_enqueued_tasks_total",
|
|
8
|
+
"Total number of enqueued tasks.",
|
|
9
|
+
["task_identifier"],
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if settings.DOCGEN_MODE or settings.TASK_PROCESSOR_MODE:
|
|
13
|
+
flagsmith_task_processor_finished_tasks_total = prometheus_client.Counter(
|
|
14
|
+
"flagsmith_task_processor_finished_tasks_total",
|
|
15
|
+
"Total number of finished tasks. Only collected by Task Processor. `task_type` label is either `recurring` or `standard`.",
|
|
16
|
+
["task_identifier", "task_type", "result"],
|
|
17
|
+
)
|
|
18
|
+
flagsmith_task_processor_task_duration_seconds = Histogram(
|
|
19
|
+
"flagsmith_task_processor_task_duration_seconds",
|
|
20
|
+
"Task processor task duration in seconds. Only collected by Task Processor. `task_type` label is either `recurring` or `standard`.",
|
|
21
|
+
["task_identifier", "task_type", "result"],
|
|
22
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Generated by Django 3.2.14 on 2022-08-02 11:25
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
import django.utils.timezone
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='Task',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
21
|
+
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
|
22
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
23
|
+
('scheduled_for', models.DateTimeField(blank=True, default=django.utils.timezone.now, null=True)),
|
|
24
|
+
('task_identifier', models.CharField(max_length=200)),
|
|
25
|
+
('serialized_args', models.TextField(blank=True, null=True)),
|
|
26
|
+
('serialized_kwargs', models.TextField(blank=True, null=True)),
|
|
27
|
+
('num_failures', models.IntegerField(default=0)),
|
|
28
|
+
],
|
|
29
|
+
options={
|
|
30
|
+
'index_together': {('scheduled_for', 'num_failures')},
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
migrations.CreateModel(
|
|
34
|
+
name='TaskRun',
|
|
35
|
+
fields=[
|
|
36
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
37
|
+
('started_at', models.DateTimeField()),
|
|
38
|
+
('finished_at', models.DateTimeField(blank=True, null=True)),
|
|
39
|
+
('result', models.CharField(blank=True, choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], db_index=True, max_length=50, null=True)),
|
|
40
|
+
('error_details', models.TextField(blank=True, null=True)),
|
|
41
|
+
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='task_runs', to='task_processor.task')),
|
|
42
|
+
],
|
|
43
|
+
),
|
|
44
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Generated by Django 3.2.14 on 2022-08-12 11:39
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('task_processor', '0001_initial'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.CreateModel(
|
|
14
|
+
name='HealthCheckModel',
|
|
15
|
+
fields=[
|
|
16
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
17
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
18
|
+
('uuid', models.UUIDField(unique=True)),
|
|
19
|
+
],
|
|
20
|
+
),
|
|
21
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 3.2.15 on 2022-08-24 13:53
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('task_processor', '0002_healthcheckmodel'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='task',
|
|
15
|
+
name='completed',
|
|
16
|
+
field=models.BooleanField(default=False),
|
|
17
|
+
),
|
|
18
|
+
migrations.AlterIndexTogether(
|
|
19
|
+
name='task',
|
|
20
|
+
index_together={('scheduled_for', 'num_failures', 'completed')},
|
|
21
|
+
),
|
|
22
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Generated by Django 3.2.15 on 2022-10-07 09:53
|
|
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
|
+
atomic = False
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
("task_processor", "0003_add_completed_to_task"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.SeparateDatabaseAndState(
|
|
18
|
+
state_operations=[
|
|
19
|
+
migrations.AlterIndexTogether(
|
|
20
|
+
name="task",
|
|
21
|
+
index_together=set(),
|
|
22
|
+
),
|
|
23
|
+
migrations.AddIndex(
|
|
24
|
+
model_name="task",
|
|
25
|
+
index=models.Index(
|
|
26
|
+
condition=models.Q(("completed", False)),
|
|
27
|
+
fields=["num_failures", "scheduled_for"],
|
|
28
|
+
name="incomplete_tasks_idx",
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
],
|
|
32
|
+
database_operations=[
|
|
33
|
+
PostgresOnlyRunSQL(
|
|
34
|
+
"DROP INDEX CONCURRENTLY task_processor_task_scheduled_for_num_failur_17d6dc77_idx;",
|
|
35
|
+
reverse_sql='CREATE INDEX "task_processor_task_scheduled_for_num_failur_17d6dc77_idx" ON "task_processor_task" ("scheduled_for", "num_failures", "completed");',
|
|
36
|
+
),
|
|
37
|
+
PostgresOnlyRunSQL(
|
|
38
|
+
'CREATE INDEX CONCURRENTLY "incomplete_tasks_idx" ON "task_processor_task" ("num_failures", "scheduled_for") WHERE NOT "completed";',
|
|
39
|
+
reverse_sql='DROP INDEX CONCURRENTLY "incomplete_tasks_idx";',
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
),
|
|
43
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Generated by Django 3.2.15 on 2022-10-07 11:16
|
|
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
|
+
atomic = False
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
("task_processor", "0004_recreate_task_indexes"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.SeparateDatabaseAndState(
|
|
18
|
+
state_operations=[
|
|
19
|
+
migrations.RemoveIndex(
|
|
20
|
+
model_name="task",
|
|
21
|
+
name="incomplete_tasks_idx",
|
|
22
|
+
),
|
|
23
|
+
migrations.AddIndex(
|
|
24
|
+
model_name="task",
|
|
25
|
+
index=models.Index(
|
|
26
|
+
condition=models.Q(
|
|
27
|
+
("completed", False), ("num_failures__lt", 3)
|
|
28
|
+
),
|
|
29
|
+
fields=["scheduled_for"],
|
|
30
|
+
name="incomplete_tasks_idx",
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
],
|
|
34
|
+
database_operations=[
|
|
35
|
+
PostgresOnlyRunSQL(
|
|
36
|
+
'DROP INDEX CONCURRENTLY "incomplete_tasks_idx";',
|
|
37
|
+
reverse_sql='CREATE INDEX CONCURRENTLY "incomplete_tasks_idx" ON "task_processor_task" ("num_failures", "scheduled_for") WHERE NOT "completed";',
|
|
38
|
+
),
|
|
39
|
+
PostgresOnlyRunSQL(
|
|
40
|
+
'CREATE INDEX CONCURRENTLY "incomplete_tasks_idx" ON "task_processor_task" ("scheduled_for") WHERE (NOT "completed" and "num_failures" < 3);',
|
|
41
|
+
reverse_sql='DROP INDEX CONCURRENTLY "incomplete_tasks_idx";',
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Generated by Django 3.2.16 on 2023-02-21 08:02
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
('task_processor', '0005_update_conditional_index_conditions'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='RecurringTask',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
|
20
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
21
|
+
('task_identifier', models.CharField(max_length=200)),
|
|
22
|
+
('serialized_args', models.TextField(blank=True, null=True)),
|
|
23
|
+
('serialized_kwargs', models.TextField(blank=True, null=True)),
|
|
24
|
+
('run_every', models.DurationField()),
|
|
25
|
+
],
|
|
26
|
+
),
|
|
27
|
+
migrations.CreateModel(
|
|
28
|
+
name='RecurringTaskRun',
|
|
29
|
+
fields=[
|
|
30
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
31
|
+
('started_at', models.DateTimeField()),
|
|
32
|
+
('finished_at', models.DateTimeField(blank=True, null=True)),
|
|
33
|
+
('result', models.CharField(blank=True, choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], db_index=True, max_length=50, null=True)),
|
|
34
|
+
('error_details', models.TextField(blank=True, null=True)),
|
|
35
|
+
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='task_runs', to='task_processor.recurringtask')),
|
|
36
|
+
],
|
|
37
|
+
options={
|
|
38
|
+
'abstract': False,
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
migrations.AddConstraint(
|
|
42
|
+
model_name='recurringtask',
|
|
43
|
+
constraint=models.UniqueConstraint(fields=('task_identifier', 'run_every'), name='unique_run_every_tasks'),
|
|
44
|
+
),
|
|
45
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 3.2.18 on 2023-04-20 02:43
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('task_processor', '0006_auto_20230221_0802'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='recurringtask',
|
|
15
|
+
name='is_locked',
|
|
16
|
+
field=models.BooleanField(default=False),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='task',
|
|
20
|
+
name='is_locked',
|
|
21
|
+
field=models.BooleanField(default=False),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Generated by Django 3.2.18 on 2023-04-20 02:45
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
from common.migrations.helpers import PostgresOnlyRunSQL
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
dependencies = [
|
|
11
|
+
("task_processor", "0007_add_is_locked"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
PostgresOnlyRunSQL.from_sql_file(
|
|
16
|
+
os.path.join(
|
|
17
|
+
os.path.dirname(__file__),
|
|
18
|
+
"sql",
|
|
19
|
+
"0008_get_tasks_to_process.sql",
|
|
20
|
+
),
|
|
21
|
+
reverse_sql="DROP FUNCTION IF EXISTS get_tasks_to_process",
|
|
22
|
+
),
|
|
23
|
+
PostgresOnlyRunSQL.from_sql_file(
|
|
24
|
+
os.path.join(
|
|
25
|
+
os.path.dirname(__file__),
|
|
26
|
+
"sql",
|
|
27
|
+
"0008_get_recurring_tasks_to_process.sql",
|
|
28
|
+
),
|
|
29
|
+
reverse_sql="DROP FUNCTION IF EXISTS get_recurringtasks_to_process",
|
|
30
|
+
),
|
|
31
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 3.2.18 on 2023-04-05 13:47
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('task_processor', '0008_add_get_task_to_process_function'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='recurringtask',
|
|
15
|
+
name='first_run_time',
|
|
16
|
+
field=models.TimeField(blank=True, null=True),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 3.2.20 on 2023-10-13 06:04
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("task_processor", "0009_add_recurring_task_run_first_run_at"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="task",
|
|
14
|
+
name="priority",
|
|
15
|
+
field=models.SmallIntegerField(
|
|
16
|
+
choices=[
|
|
17
|
+
(100, "Lower"),
|
|
18
|
+
(75, "Low"),
|
|
19
|
+
(50, "Normal"),
|
|
20
|
+
(25, "High"),
|
|
21
|
+
(0, "Highest"),
|
|
22
|
+
],
|
|
23
|
+
default=None,
|
|
24
|
+
null=True,
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 3.2.20 on 2023-10-13 04:44
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
from common.migrations.helpers import PostgresOnlyRunSQL
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
dependencies = [
|
|
11
|
+
("task_processor", "0010_task_priority"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
PostgresOnlyRunSQL.from_sql_file(
|
|
16
|
+
os.path.join(
|
|
17
|
+
os.path.dirname(__file__),
|
|
18
|
+
"sql",
|
|
19
|
+
"0011_get_tasks_to_process.sql",
|
|
20
|
+
),
|
|
21
|
+
reverse_sql=os.path.join(
|
|
22
|
+
os.path.dirname(__file__),
|
|
23
|
+
"sql",
|
|
24
|
+
"0008_get_tasks_to_process.sql",
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Generated by Django 3.2.23 on 2025-01-06 04:51
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from common.migrations.helpers import PostgresOnlyRunSQL
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
("task_processor", "0011_add_priority_to_get_tasks_to_process"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.AddField(
|
|
18
|
+
model_name="recurringtask",
|
|
19
|
+
name="locked_at",
|
|
20
|
+
field=models.DateTimeField(blank=True, null=True),
|
|
21
|
+
),
|
|
22
|
+
migrations.AddField(
|
|
23
|
+
model_name="recurringtask",
|
|
24
|
+
name="timeout",
|
|
25
|
+
field=models.DurationField(default=datetime.timedelta(minutes=30)),
|
|
26
|
+
),
|
|
27
|
+
migrations.AddField(
|
|
28
|
+
model_name="task",
|
|
29
|
+
name="timeout",
|
|
30
|
+
field=models.DurationField(blank=True, null=True),
|
|
31
|
+
),
|
|
32
|
+
PostgresOnlyRunSQL.from_sql_file(
|
|
33
|
+
os.path.join(
|
|
34
|
+
os.path.dirname(__file__),
|
|
35
|
+
"sql",
|
|
36
|
+
"0012_get_recurringtasks_to_process.sql",
|
|
37
|
+
),
|
|
38
|
+
reverse_sql="DROP FUNCTION IF EXISTS get_recurringtasks_to_process()",
|
|
39
|
+
),
|
|
40
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Generated by Django 4.2.18 on 2025-04-03 07:34
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.utils.timezone
|
|
5
|
+
import os
|
|
6
|
+
from common.migrations.helpers import PostgresOnlyRunSQL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("task_processor", "0012_add_locked_at_and_timeout"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.AddField(
|
|
17
|
+
model_name="recurringtask",
|
|
18
|
+
name="last_picked_at",
|
|
19
|
+
field=models.DateTimeField(blank=True, null=True),
|
|
20
|
+
preserve_default=False,
|
|
21
|
+
),
|
|
22
|
+
PostgresOnlyRunSQL.from_sql_file(
|
|
23
|
+
os.path.join(
|
|
24
|
+
os.path.dirname(__file__),
|
|
25
|
+
"sql",
|
|
26
|
+
"0013_get_recurringtasks_to_process.sql",
|
|
27
|
+
),
|
|
28
|
+
reverse_sql=os.path.join(
|
|
29
|
+
os.path.dirname(__file__),
|
|
30
|
+
"sql",
|
|
31
|
+
"0012_get_recurringtasks_to_process.sql",
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION get_recurringtasks_to_process(num_tasks integer)
|
|
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
|
+
WHERE is_locked = FALSE
|
|
11
|
+
ORDER BY id
|
|
12
|
+
LIMIT num_tasks
|
|
13
|
+
-- Select for update to ensure that no other workers can select these tasks while in this transaction block
|
|
14
|
+
FOR UPDATE SKIP LOCKED
|
|
15
|
+
LOOP
|
|
16
|
+
-- Lock every selected task(by updating `is_locked` to true)
|
|
17
|
+
UPDATE task_processor_recurringtask
|
|
18
|
+
-- Lock this row by setting is_locked True, so that no other workers can select these tasks after this
|
|
19
|
+
-- transaction is complete (but the tasks are still being executed by the current worker)
|
|
20
|
+
SET is_locked = TRUE
|
|
21
|
+
WHERE id = row_to_return.id;
|
|
22
|
+
-- If we don't explicitly update the `is_locked` column here, the client will receive the row that is actually locked but has the `is_locked` value set to `False`.
|
|
23
|
+
row_to_return.is_locked := TRUE;
|
|
24
|
+
RETURN NEXT row_to_return;
|
|
25
|
+
END LOOP;
|
|
26
|
+
|
|
27
|
+
RETURN;
|
|
28
|
+
END;
|
|
29
|
+
$$ LANGUAGE plpgsql
|
|
30
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION get_tasks_to_process(num_tasks integer)
|
|
2
|
+
RETURNS SETOF task_processor_task AS $$
|
|
3
|
+
DECLARE
|
|
4
|
+
row_to_return task_processor_task;
|
|
5
|
+
BEGIN
|
|
6
|
+
-- Select the tasks that needs to be processed
|
|
7
|
+
FOR row_to_return IN
|
|
8
|
+
SELECT *
|
|
9
|
+
FROM task_processor_task
|
|
10
|
+
WHERE num_failures < 3 AND scheduled_for < NOW() AND completed = FALSE AND is_locked = FALSE
|
|
11
|
+
ORDER BY scheduled_for ASC, created_at ASC
|
|
12
|
+
LIMIT num_tasks
|
|
13
|
+
-- Select for update to ensure that no other workers can select these tasks while in this transaction block
|
|
14
|
+
FOR UPDATE SKIP LOCKED
|
|
15
|
+
LOOP
|
|
16
|
+
-- Lock every selected task(by updating `is_locked` to true)
|
|
17
|
+
UPDATE task_processor_task
|
|
18
|
+
-- Lock this row by setting is_locked True, so that no other workers can select these tasks after this
|
|
19
|
+
-- transaction is complete (but the tasks are still being executed by the current worker)
|
|
20
|
+
SET is_locked = TRUE
|
|
21
|
+
WHERE id = row_to_return.id;
|
|
22
|
+
-- If we don't explicitly update the `is_locked` column here, the client will receive the row that is actually locked but has the `is_locked` value set to `False`.
|
|
23
|
+
row_to_return.is_locked := TRUE;
|
|
24
|
+
RETURN NEXT row_to_return;
|
|
25
|
+
END LOOP;
|
|
26
|
+
|
|
27
|
+
RETURN;
|
|
28
|
+
END;
|
|
29
|
+
$$ LANGUAGE plpgsql
|
|
30
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION get_tasks_to_process(num_tasks integer)
|
|
2
|
+
RETURNS SETOF task_processor_task AS $$
|
|
3
|
+
DECLARE
|
|
4
|
+
row_to_return task_processor_task;
|
|
5
|
+
BEGIN
|
|
6
|
+
-- Select the tasks that needs to be processed
|
|
7
|
+
FOR row_to_return IN
|
|
8
|
+
SELECT *
|
|
9
|
+
FROM task_processor_task
|
|
10
|
+
WHERE num_failures < 3 AND scheduled_for < NOW() AND completed = FALSE AND is_locked = FALSE
|
|
11
|
+
ORDER BY priority ASC, scheduled_for ASC, created_at ASC
|
|
12
|
+
LIMIT num_tasks
|
|
13
|
+
-- Select for update to ensure that no other workers can select these tasks while in this transaction block
|
|
14
|
+
FOR UPDATE SKIP LOCKED
|
|
15
|
+
LOOP
|
|
16
|
+
-- Lock every selected task(by updating `is_locked` to true)
|
|
17
|
+
UPDATE task_processor_task
|
|
18
|
+
-- Lock this row by setting is_locked True, so that no other workers can select these tasks after this
|
|
19
|
+
-- transaction is complete (but the tasks are still being executed by the current worker)
|
|
20
|
+
SET is_locked = TRUE
|
|
21
|
+
WHERE id = row_to_return.id;
|
|
22
|
+
-- If we don't explicitly update the `is_locked` column here, the client will receive the row that is actually locked but has the `is_locked` value set to `False`.
|
|
23
|
+
row_to_return.is_locked := TRUE;
|
|
24
|
+
RETURN NEXT row_to_return;
|
|
25
|
+
END LOOP;
|
|
26
|
+
|
|
27
|
+
RETURN;
|
|
28
|
+
END;
|
|
29
|
+
$$ LANGUAGE plpgsql
|
|
30
|
+
|
|
@@ -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
|
+
-- Add one minute to the timeout as a grace period for overhead
|
|
11
|
+
WHERE is_locked = FALSE OR (locked_at IS NOT NULL AND locked_at < NOW() - timeout + INTERVAL '1 minute')
|
|
12
|
+
ORDER BY id
|
|
13
|
+
LIMIT 1
|
|
14
|
+
-- Select for update to ensure that no other workers can select these tasks while in this transaction block
|
|
15
|
+
FOR UPDATE SKIP LOCKED
|
|
16
|
+
LOOP
|
|
17
|
+
-- Lock every selected task(by updating `is_locked` to true)
|
|
18
|
+
UPDATE task_processor_recurringtask
|
|
19
|
+
-- Lock this row by setting is_locked True, so that no other workers can select these tasks after this
|
|
20
|
+
-- transaction is complete (but the tasks are still being executed by the current worker)
|
|
21
|
+
SET is_locked = TRUE, locked_at = NOW()
|
|
22
|
+
WHERE id = row_to_return.id;
|
|
23
|
+
-- If we don't explicitly update the columns here, the client will receive a row
|
|
24
|
+
-- that is locked but still shows `is_locked` as `False` and `locked_at` as `None`.
|
|
25
|
+
row_to_return.is_locked := TRUE;
|
|
26
|
+
row_to_return.locked_at := NOW();
|
|
27
|
+
RETURN NEXT row_to_return;
|
|
28
|
+
END LOOP;
|
|
29
|
+
|
|
30
|
+
RETURN;
|
|
31
|
+
END;
|
|
32
|
+
$$ LANGUAGE plpgsql
|
|
33
|
+
|