Skip to content

Commit eb12053

Browse files
dklibanclaude
andcommitted
Adds 'redis' WORKER_TYPE
This adds WORKER_TYPE setting. The default value is 'pulpcore'. When 'redis' is selected, the tasking system uses Redis to lock resources. Redis workers produce less load on the PostgreSQL database. closes: #7210 Generated By: Claude Code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ed5719d commit eb12053

16 files changed

Lines changed: 1913 additions & 7 deletions

File tree

.github/workflows/scripts/before_install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ if [ "$TEST" = "azure" ]; then
105105
image: "mcr.microsoft.com/azure-storage/azurite"
106106
command: "azurite-blob --skipApiVersionCheck --blobHost 0.0.0.0"
107107
azure_test: true
108-
pulp_scenario_settings: {"MEDIA_ROOT": "", "STORAGES": {"default": {"BACKEND": "storages.backends.azure_storage.AzureStorage", "OPTIONS": {"account_key": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", "account_name": "devstoreaccount1", "azure_container": "pulp-test", "connection_string": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://ci-azurite:10000/devstoreaccount1;", "expiration_secs": 120, "location": "pulp3", "overwrite_files": true}}, "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}}, "api_root_rewrite_header": "X-API-Root", "content_origin": null, "domain_enabled": true, "rest_framework__default_authentication_classes": "@merge pulpcore.app.authentication.PulpRemoteUserAuthentication", "rest_framework__default_permission_classes": ["pulpcore.plugin.access_policy.DefaultAccessPolicy"]}
108+
pulp_scenario_settings: {"MEDIA_ROOT": "", "STORAGES": {"default": {"BACKEND": "storages.backends.azure_storage.AzureStorage", "OPTIONS": {"account_key": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", "account_name": "devstoreaccount1", "azure_container": "pulp-test", "connection_string": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://ci-azurite:10000/devstoreaccount1;", "expiration_secs": 120, "location": "pulp3", "overwrite_files": true}}, "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}}, "api_root_rewrite_header": "X-API-Root", "content_origin": null, "domain_enabled": true, "rest_framework__default_authentication_classes": "@merge pulpcore.app.authentication.PulpRemoteUserAuthentication", "rest_framework__default_permission_classes": ["pulpcore.plugin.access_policy.DefaultAccessPolicy"], "task_diagnostics": ["memory"], "worker_type": "redis"}
109109
pulp_scenario_env: {}
110110
VARSYAML
111111
fi

CHANGES/7210.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added WORKER_TYPE setting. Defaults to 'pulpcore'. 'redis' is also available.

docs/admin/reference/settings.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,20 @@ The number of seconds before a worker should be considered lost.
413413

414414
Defaults to `30` seconds.
415415

416+
### WORKER\_TYPE
417+
418+
The worker implementation to use for task execution.
419+
420+
Available options:
421+
422+
- `"pulpcore"` (default): Uses PostgreSQL advisory locks for task coordination. This is the traditional worker implementation.
423+
- `"redis"`: Uses Redis distributed locks for task coordination. This implementation produces less load on the DB.
424+
425+
!!! note
426+
The Redis worker requires a Redis server to be configured and accessible.
427+
428+
Defaults to `"pulpcore"`.
429+
416430
### WORKING\_DIRECTORY
417431

418432
The directory used by workers to stage files temporarily.

pulpcore/app/redis_connection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
_a_conn = None
88

99

10+
def _redis_is_needed():
11+
return settings.get("CACHE_ENABLED") or settings.get("WORKER_TYPE") == "redis"
12+
13+
1014
def _get_connection_from_class(redis_class):
11-
if not settings.get("CACHE_ENABLED"):
15+
if not _redis_is_needed():
1216
return None
1317
redis_url = settings.get("REDIS_URL")
1418
if redis_url is not None:

pulpcore/app/settings.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@
292292
CONTENT_APP_TTL = 30
293293
WORKER_TTL = 30
294294

295+
# Worker implementation type
296+
# Options: "pulpcore" (default, PostgreSQL advisory locks) or "redis" (Redis distributed locks)
297+
WORKER_TYPE = "pulpcore"
298+
295299
# Seconds for a task to finish on semi graceful worker shutdown (approx)
296300
# On SIGHUP, SIGTERM the currently running task will be awaited forever.
297301
# On SIGINT, this value represents the time before the worker will attempt to kill the subprocess.
@@ -457,6 +461,31 @@
457461
"for more information."
458462
)
459463

464+
worker_type_allowed_values_validator = Validator(
465+
"WORKER_TYPE",
466+
is_in=["pulpcore", "redis"],
467+
messages={"operations": "WORKER_TYPE must be either 'pulpcore' or 'redis', got '{value}'."},
468+
)
469+
470+
worker_type_redis_validator_condition = Validator("WORKER_TYPE", eq="redis")
471+
worker_type_redis_url_validator = Validator(
472+
"REDIS_URL", must_exist=True, when=worker_type_redis_validator_condition
473+
)
474+
worker_type_redis_host_validator = Validator(
475+
"REDIS_HOST", must_exist=True, when=worker_type_redis_validator_condition
476+
)
477+
worker_type_redis_port_validator = Validator(
478+
"REDIS_PORT", must_exist=True, when=worker_type_redis_validator_condition
479+
)
480+
worker_type_redis_validator = worker_type_redis_url_validator | (
481+
worker_type_redis_host_validator & worker_type_redis_port_validator
482+
)
483+
worker_type_redis_validator.messages["combined"] = (
484+
"WORKER_TYPE is set to 'redis' but it requires REDIS to be configured. Please check "
485+
"https://pulpproject.org/pulpcore/docs/admin/reference/settings/?h=settings#redis-settings "
486+
"for more information."
487+
)
488+
460489
sha256_validator = Validator(
461490
"ALLOWED_CONTENT_CHECKSUMS",
462491
cont="sha256",
@@ -551,6 +580,8 @@ def otel_middleware_hook(settings):
551580
validators=[
552581
api_root_validator,
553582
cache_validator,
583+
worker_type_allowed_values_validator,
584+
worker_type_redis_validator,
554585
sha256_validator,
555586
storage_validator,
556587
unknown_algs_validator,

pulpcore/app/tasks/test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import asyncio
22
import backoff
3+
import os
4+
import signal
35
import time
46
from pulpcore.app.models import TaskGroup
57
from pulpcore.tasking.tasks import dispatch
@@ -32,3 +34,44 @@ def dummy_group_task(inbetween=3, intervals=None):
3234
for interval in intervals:
3335
dispatch(sleep, args=(interval,), task_group=task_group)
3436
time.sleep(inbetween)
37+
38+
39+
def missing_worker():
40+
"""
41+
Simulates a worker crash by sending SIGKILL to parent process and itself.
42+
43+
This task is used for testing worker cleanup behavior when a worker
44+
unexpectedly dies while executing a task.
45+
"""
46+
parent_pid = os.getppid()
47+
current_pid = os.getpid()
48+
49+
# Kill parent process (the worker)
50+
os.kill(parent_pid, signal.SIGKILL)
51+
52+
# Kill current process (the task subprocess)
53+
os.kill(current_pid, signal.SIGKILL)
54+
55+
56+
def failing_task(error_message="Task intentionally failed"):
57+
"""
58+
A task that always raises a RuntimeError.
59+
60+
This task is used for testing error handling in worker task execution.
61+
62+
Args:
63+
error_message (str): The error message to include in the RuntimeError
64+
"""
65+
raise RuntimeError(error_message)
66+
67+
68+
async def afailing_task(error_message="Task intentionally failed"):
69+
"""
70+
An async task that always raises a RuntimeError.
71+
72+
This task is used for testing error handling in immediate task execution.
73+
74+
Args:
75+
error_message (str): The error message to include in the RuntimeError
76+
"""
77+
raise RuntimeError(error_message)

pulpcore/app/viewsets/task.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
CreatedResourcesFilter,
4343
)
4444
from pulpcore.constants import TASK_INCOMPLETE_STATES, TASK_STATES
45-
from pulpcore.tasking.tasks import dispatch, cancel_task, cancel_task_group
45+
from pulpcore.tasking.tasks import cancel_task, cancel_task_group, dispatch
4646
from pulpcore.app.role_util import get_objects_for_user
4747

4848

@@ -228,6 +228,7 @@ def partial_update(self, request, pk=None, partial=True):
228228

229229
task = self.get_object()
230230
task = cancel_task(task.pk)
231+
231232
# Check whether task is actually canceled
232233
http_status = (
233234
None
@@ -346,6 +347,7 @@ def partial_update(self, request, pk=None, partial=True):
346347
).count()
347348
):
348349
raise PermissionDenied()
350+
349351
task_group = cancel_task_group(task_group.pk)
350352
# Check whether task group is actually canceled
351353
serializer = TaskGroupSerializer(task_group, context={"request": request})

pulpcore/pytest_plugin.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ def pytest_collection_modifyitems(config, items):
5555
if "nightly" in item.keywords:
5656
item.add_marker(skip_nightly)
5757

58+
# Skip long_running tests if --timeout is below 600
59+
timeout = config.getoption("--timeout", default=None)
60+
if timeout is None or float(timeout) < 600:
61+
skip_long = pytest.mark.skip(reason="needs --timeout >= 600 to run")
62+
for item in items:
63+
if "long_running" in item.keywords:
64+
item.add_marker(skip_long)
65+
5866

5967
class PulpTaskTimeoutError(Exception):
6068
"""Exception to describe task and taskgroup timeout errors."""
@@ -81,6 +89,10 @@ def pytest_configure(config):
8189
"markers",
8290
"nightly: marks tests as intended to run during the nightly CI run",
8391
)
92+
config.addinivalue_line(
93+
"markers",
94+
"long_running: marks tests that need a long pytest timeout (--timeout >= 600)",
95+
)
8496

8597

8698
@pytest.fixture(scope="session")

pulpcore/tasking/_util.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@
2323
configure_periodic_telemetry,
2424
)
2525
from pulpcore.constants import TASK_FINAL_STATES, TASK_STATES
26-
from pulpcore.tasking.tasks import dispatch, execute_task
26+
from pulpcore.tasking.tasks import dispatch
27+
28+
# Conditionally import execute_task based on WORKER_TYPE
29+
if settings.WORKER_TYPE == "redis":
30+
from pulpcore.tasking.redis_tasks import execute_task
31+
else:
32+
from pulpcore.tasking.tasks import execute_task
33+
2734

2835
_logger = logging.getLogger(__name__)
2936

pulpcore/tasking/entrypoint.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from django.conf import settings # noqa: E402: module level not at top
1212
from pulpcore.tasking.worker import PulpcoreWorker # noqa: E402: module level not at top
13+
from pulpcore.tasking.redis_worker import RedisWorker # noqa: E402: module level not at top
1314

1415
_logger = logging.getLogger(__name__)
1516

@@ -58,6 +59,16 @@ def worker(
5859
if name_template:
5960
settings.set("WORKER_NAME_TEMPLATE", name_template)
6061

61-
_logger.info("Starting distributed type worker")
62+
worker_type = settings.WORKER_TYPE
63+
_logger.info("Starting %s worker", worker_type)
6264

63-
PulpcoreWorker(auxiliary=auxiliary).run(burst=burst)
65+
if worker_type == "redis":
66+
if auxiliary:
67+
_logger.warning(
68+
"RedisWorker does not support auxiliary mode, ignoring --auxiliary flag"
69+
)
70+
RedisWorker().run(burst=burst)
71+
elif worker_type == "pulpcore":
72+
PulpcoreWorker(auxiliary=auxiliary).run(burst=burst)
73+
else:
74+
raise ValueError(f"Invalid WORKER_TYPE: {worker_type}. Must be 'pulpcore' or 'redis'.")

0 commit comments

Comments
 (0)