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.
Files changed (92) hide show
  1. common/__init__.py +0 -0
  2. common/core/__init__.py +6 -0
  3. common/core/app.py +6 -0
  4. common/core/cli/__init__.py +0 -0
  5. common/core/cli/healthcheck.py +120 -0
  6. common/core/logging.py +24 -0
  7. common/core/main.py +105 -0
  8. common/core/management/__init__.py +0 -0
  9. common/core/management/commands/__init__.py +0 -0
  10. common/core/management/commands/docgen.py +63 -0
  11. common/core/management/commands/start.py +61 -0
  12. common/core/management/commands/waitfordb.py +87 -0
  13. common/core/metrics.py +25 -0
  14. common/core/middleware.py +22 -0
  15. common/core/templates/docgen-metrics.md +22 -0
  16. common/core/urls.py +17 -0
  17. common/core/utils.py +239 -0
  18. common/core/views.py +27 -0
  19. common/environments/permissions.py +15 -0
  20. common/features/__init__.py +0 -0
  21. common/features/multivariate/__init__.py +0 -0
  22. common/features/multivariate/serializers.py +19 -0
  23. common/features/serializers.py +68 -0
  24. common/features/versioning/__init__.py +0 -0
  25. common/features/versioning/serializers.py +13 -0
  26. common/gunicorn/__init__.py +0 -0
  27. common/gunicorn/conf.py +18 -0
  28. common/gunicorn/constants.py +23 -0
  29. common/gunicorn/logging.py +120 -0
  30. common/gunicorn/metrics.py +26 -0
  31. common/gunicorn/middleware.py +30 -0
  32. common/gunicorn/utils.py +104 -0
  33. common/migrations/__init__.py +0 -0
  34. common/migrations/helpers/__init__.py +9 -0
  35. common/migrations/helpers/postgres_helpers.py +41 -0
  36. common/organisations/permissions.py +10 -0
  37. common/projects/permissions.py +40 -0
  38. common/prometheus/__init__.py +3 -0
  39. common/prometheus/utils.py +38 -0
  40. common/py.typed +0 -0
  41. common/test_tools/__init__.py +11 -0
  42. common/test_tools/plugin.py +139 -0
  43. common/test_tools/types.py +56 -0
  44. common/test_tools/utils.py +11 -0
  45. common/types.py +45 -0
  46. flagsmith_common-2.2.4.dist-info/METADATA +196 -0
  47. flagsmith_common-2.2.4.dist-info/RECORD +92 -0
  48. flagsmith_common-2.2.4.dist-info/WHEEL +4 -0
  49. flagsmith_common-2.2.4.dist-info/entry_points.txt +6 -0
  50. flagsmith_common-2.2.4.dist-info/licenses/LICENSE +28 -0
  51. task_processor/__init__.py +0 -0
  52. task_processor/admin.py +38 -0
  53. task_processor/apps.py +47 -0
  54. task_processor/decorators.py +209 -0
  55. task_processor/exceptions.py +28 -0
  56. task_processor/health.py +44 -0
  57. task_processor/managers.py +18 -0
  58. task_processor/metrics.py +22 -0
  59. task_processor/migrations/0001_initial.py +44 -0
  60. task_processor/migrations/0002_healthcheckmodel.py +21 -0
  61. task_processor/migrations/0003_add_completed_to_task.py +22 -0
  62. task_processor/migrations/0004_recreate_task_indexes.py +43 -0
  63. task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
  64. task_processor/migrations/0006_auto_20230221_0802.py +45 -0
  65. task_processor/migrations/0007_add_is_locked.py +23 -0
  66. task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
  67. task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
  68. task_processor/migrations/0010_task_priority.py +27 -0
  69. task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
  70. task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
  71. task_processor/migrations/0013_add_last_picked_at.py +34 -0
  72. task_processor/migrations/__init__.py +0 -0
  73. task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
  74. task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
  75. task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
  76. task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
  77. task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
  78. task_processor/migrations/sql/__init__.py +0 -0
  79. task_processor/models.py +237 -0
  80. task_processor/monitoring.py +12 -0
  81. task_processor/processor.py +202 -0
  82. task_processor/py.typed +0 -0
  83. task_processor/routers.py +55 -0
  84. task_processor/serializers.py +7 -0
  85. task_processor/task_registry.py +90 -0
  86. task_processor/task_run_method.py +7 -0
  87. task_processor/tasks.py +71 -0
  88. task_processor/threads.py +128 -0
  89. task_processor/types.py +18 -0
  90. task_processor/urls.py +5 -0
  91. task_processor/utils.py +71 -0
  92. task_processor/views.py +20 -0
@@ -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
+