|
@@ -11,6 +11,7 @@ from uuid import uuid4
|
|
|
import grpc
|
|
|
import orjson
|
|
|
from django.conf import settings
|
|
|
+from django.core.cache import cache
|
|
|
from sentry_protos.sentry.v1.taskworker_pb2 import (
|
|
|
TASK_ACTIVATION_STATUS_COMPLETE,
|
|
|
TASK_ACTIVATION_STATUS_FAILURE,
|
|
@@ -20,6 +21,7 @@ from sentry_protos.sentry.v1.taskworker_pb2 import (
|
|
|
|
|
|
from sentry.taskworker.client import TaskworkerClient
|
|
|
from sentry.taskworker.registry import taskregistry
|
|
|
+from sentry.taskworker.task import Task
|
|
|
from sentry.utils import metrics
|
|
|
|
|
|
logger = logging.getLogger("sentry.taskworker.worker")
|
|
@@ -35,6 +37,14 @@ def _process_activation(
|
|
|
taskregistry.get(namespace).get(task_name)(*args, **kwargs)
|
|
|
|
|
|
|
|
|
+AT_MOST_ONCE_TIMEOUT = 60 * 60 * 24 # 1 day
|
|
|
+
|
|
|
+
|
|
|
+def get_at_most_once_key(namespace: str, taskname: str, task_id: str) -> str:
|
|
|
+ # tw:amo -> taskworker:at_most_once
|
|
|
+ return f"tw:amo:{namespace}:{taskname}:{task_id}"
|
|
|
+
|
|
|
+
|
|
|
class TaskWorker:
|
|
|
"""
|
|
|
A TaskWorker fetches tasks from a taskworker RPC host and handles executing task activations.
|
|
@@ -128,13 +138,13 @@ class TaskWorker:
|
|
|
metrics.incr("taskworker.worker.get_task.success")
|
|
|
return activation
|
|
|
|
|
|
- def _known_task(self, activation: TaskActivation) -> bool:
|
|
|
+ def _get_known_task(self, activation: TaskActivation) -> Task[Any, Any] | None:
|
|
|
if not taskregistry.contains(activation.namespace):
|
|
|
logger.error(
|
|
|
"taskworker.invalid_namespace",
|
|
|
extra={"namespace": activation.namespace, "taskname": activation.taskname},
|
|
|
)
|
|
|
- return False
|
|
|
+ return None
|
|
|
|
|
|
namespace = taskregistry.get(activation.namespace)
|
|
|
if not namespace.contains(activation.taskname):
|
|
@@ -142,12 +152,13 @@ class TaskWorker:
|
|
|
"taskworker.invalid_taskname",
|
|
|
extra={"namespace": activation.namespace, "taskname": activation.taskname},
|
|
|
)
|
|
|
- return False
|
|
|
- return True
|
|
|
+ return None
|
|
|
+ return namespace.get(activation.taskname)
|
|
|
|
|
|
def process_task(self, activation: TaskActivation) -> TaskActivation | None:
|
|
|
assert self._pool
|
|
|
- if not self._known_task(activation):
|
|
|
+ task = self._get_known_task(activation)
|
|
|
+ if not task:
|
|
|
metrics.incr(
|
|
|
"taskworker.worker.unknown_task",
|
|
|
tags={"namespace": activation.namespace, "taskname": activation.taskname},
|
|
@@ -158,7 +169,18 @@ class TaskWorker:
|
|
|
status=TASK_ACTIVATION_STATUS_FAILURE,
|
|
|
)
|
|
|
|
|
|
- # TODO(taskworker): Add at_most_once checks
|
|
|
+ if task.at_most_once:
|
|
|
+ key = get_at_most_once_key(activation.namespace, activation.taskname, activation.id)
|
|
|
+ if cache.add(key, "1", timeout=AT_MOST_ONCE_TIMEOUT): # The key didn't exist
|
|
|
+ metrics.incr(
|
|
|
+ "taskworker.task.at_most_once.executed", tags={"task": activation.taskname}
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ metrics.incr(
|
|
|
+ "taskworker.worker.at_most_once.skipped", tags={"task": activation.taskname}
|
|
|
+ )
|
|
|
+ return None
|
|
|
+
|
|
|
processing_timeout = activation.processing_deadline_duration
|
|
|
namespace = taskregistry.get(activation.namespace)
|
|
|
next_state = TASK_ACTIVATION_STATUS_FAILURE
|