Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-01 09:33:40

0001 """
0002 Async request handlers for Data Carousel operations, registered into processor.HANDLERS.
0003 
0004 The operations themselves live in pandaserver.taskbuffer.data_carousel_ops and are shared
0005 with the synchronous endpoints of pandaserver.api.v1.data_carousel_api. These handlers only
0006 translate between the async request framework and those operations.
0007 
0008 Data Carousel operations mutate DDM rules and Data Carousel requests, so they must never be
0009 run twice for a single request: a failed operation is reported as a terminal "done" result
0010 holding success=False, and an unexpected exception is written as a non-retriable "failed"
0011 result. While an operation is in flight a heartbeat keeps its result row fresh so
0012 recover_stale_results doesn't hand it to another machine.
0013 """
0014 
0015 import functools
0016 import json
0017 import threading
0018 import traceback
0019 
0020 from pandaserver.taskbuffer.db_proxy_mods.async_request_module import (
0021     PARAMETER_META_KEYS,
0022 )
0023 
0024 # how often the heartbeat refreshes started_at of a running result row
0025 _HEARTBEAT_INTERVAL_SECONDS = 60
0026 
0027 _dcif = None
0028 _dcif_lock = threading.Lock()
0029 
0030 
0031 def _get_dcif(tb):
0032     """
0033     Get the DataCarouselInterface, creating it on first use.
0034 
0035     The import and the constructor are expensive (Rucio, iDDS and polars imports, RSE listing
0036     and config loading), so a daemon that never gets a Data Carousel request never pays for it.
0037 
0038     Only the task buffer is passed: the constructor sets its own ddmIF, so the second argument
0039     other JEDI callers pass would be ignored anyway.
0040 
0041     Args:
0042         tb(TaskBuffer): task buffer to build the interface on; the JEDI taskBufferIF when the
0043             operations run under pandajedi.jedidog.AsyncRequestWatchDog
0044 
0045     Returns:
0046         DataCarouselInterface: shared interface instance
0047     """
0048     global _dcif
0049     with _dcif_lock:
0050         if _dcif is None:
0051             from pandaserver.taskbuffer.DataCarousel import DataCarouselInterface
0052 
0053             _dcif = DataCarouselInterface(tb)
0054         return _dcif
0055 
0056 
0057 class _ResultHeartbeat:
0058     """
0059     Context manager refreshing started_at of a running result row until the operation finishes.
0060 
0061     processor.run resets rows that have been running for longer than its stale threshold back to
0062     pending, which for a mutating operation would mean executing it a second time on another
0063     machine. Refreshing started_at keeps a legitimately slow operation from looking stale.
0064     """
0065 
0066     def __init__(self, tb, request_id, machine_name, tmp_logger):
0067         self._tb = tb
0068         self._request_id = request_id
0069         self._machine_name = machine_name
0070         self._tmp_logger = tmp_logger
0071         self._stop_event = threading.Event()
0072         self._thread = None
0073 
0074     def _beat(self):
0075         while not self._stop_event.wait(_HEARTBEAT_INTERVAL_SECONDS):
0076             try:
0077                 if not self._tb.touch_async_result(self._request_id, self._machine_name):
0078                     # the row is no longer running, so this operation may already have been handed
0079                     # to another machine by recover_stale_results; keep going but make it visible
0080                     self._tmp_logger.warning(f"heartbeat did not refresh the result row of machine={self._machine_name}; the claim may have been lost")
0081             except Exception as e:
0082                 self._tmp_logger.warning(f"heartbeat failed with {e}")
0083 
0084     def __enter__(self):
0085         self._thread = threading.Thread(target=self._beat, daemon=True)
0086         self._thread.start()
0087         return self
0088 
0089     def __exit__(self, exc_type, exc_value, exc_traceback):
0090         self._stop_event.set()
0091         self._thread.join(timeout=5)
0092         return False
0093 
0094 
0095 def _handle(operation_name, row, tb, tmp_logger, result_machine):
0096     """
0097     Run one Data Carousel operation and store its outcome as the request's result.
0098 
0099     Args:
0100         operation_name(str): key in data_carousel_ops.OPERATIONS
0101         row(dict): the async_requests row to process
0102         tb(TaskBuffer): task buffer
0103         tmp_logger(LogWrapper): logger of the processing cycle
0104         result_machine(str): machine_name the result row is keyed by
0105     """
0106     from pandaserver.taskbuffer import data_carousel_ops
0107 
0108     request_id = row["request_id"]
0109     try:
0110         operation = data_carousel_ops.OPERATIONS[operation_name]
0111         parameters = json.loads(row["parameters"] or "{}")
0112         kwargs = {key: value for key, value in parameters.items() if key not in PARAMETER_META_KEYS}
0113         tmp_logger.debug(f"running {operation_name} with {kwargs}")
0114         with _ResultHeartbeat(tb, request_id, result_machine, tmp_logger):
0115             success, message, data = operation(_get_dcif(tb), **kwargs)
0116     except Exception:
0117         # the operation crashed; never retry since it may have partially applied
0118         err_msg = traceback.format_exc()
0119         tmp_logger.error(f"failed to run {operation_name} with {err_msg}")
0120         tb.finish_async_result(request_id, result_machine, "failed", error_msg=err_msg, retriable=False)
0121         return
0122 
0123     # an operation that ran and reported failure is a terminal result, not a retriable error
0124     tmp_logger.debug(f"{operation_name} returned success={success} message={message}")
0125     tb.finish_async_result(
0126         request_id,
0127         result_machine,
0128         "done",
0129         result=json.dumps({"success": success, "message": message, "data": data}),
0130     )
0131 
0132 
0133 # names listed here rather than taken from data_carousel_ops.OPERATIONS to keep this module's
0134 # import cheap; an unknown name would be reported as a failed result by _handle
0135 _OPERATION_NAMES = (
0136     "change_staging_destination",
0137     "change_staging_source",
0138     "force_to_staging",
0139     "retire_unused",
0140 )
0141 
0142 # request_type -> handler; the type is the operation name prefixed with data_carousel_api.REQUEST_TYPE_PREFIX
0143 HANDLERS = {f"dc_{operation_name}": functools.partial(_handle, operation_name) for operation_name in _OPERATION_NAMES}