2424
2525"""Job module with functions for job handling."""
2626
27+ from __future__ import annotations
28+
2729import os
2830import time
2931import hashlib
3032import logging
3133import queue
3234import random
3335from collections import namedtuple
34- from json import dumps
36+ from json import (
37+ dumps ,
38+ loads
39+ )
3540from glob import glob
36- from typing import Any
41+ from typing import (
42+ Any ,
43+ Callable ,
44+ Mapping ,
45+ MutableMapping ,
46+ Optional ,
47+ Tuple
48+ )
3749from urllib .parse import parse_qsl
3850
3951from pilot .common .errorcodes import ErrorCodes
94106 find_text_files ,
95107 get_total_input_size ,
96108 is_json ,
109+ read_json ,
97110 remove ,
98111 tail ,
99112 write_file ,
152165 update_modelstring
153166)
154167
168+ JsonObject = MutableMapping [str , Any ]
169+ ReadJsonFn = Callable [[str ], Optional [Mapping [str , Any ]]]
170+
155171errors = ErrorCodes ()
156172logger = logging .getLogger (__name__ )
157173pilot_cache = get_pilot_cache ()
@@ -2842,6 +2858,97 @@ def queue_monitor(queues: namedtuple, traces: Any, args: object): # noqa: C901
28422858 logger .info ('[job] queue monitor thread has finished' )
28432859
28442860
2861+ def load_metadata_dict (metadata : Optional [str ]) -> JsonObject :
2862+ """
2863+ Parse the metadata JSON string into a mutable dictionary.
2864+
2865+ Args:
2866+ metadata: Metadata as a JSON string (or None/empty).
2867+
2868+ Returns:
2869+ A mutable dictionary representation of the metadata. If `metadata` is
2870+ missing or invalid JSON, returns an empty dictionary.
2871+ """
2872+ if not metadata :
2873+ return {}
2874+
2875+ try :
2876+ parsed = loads (metadata )
2877+ except Exception as error : # pragma: no cover (logger side-effect)
2878+ logger .warning (f"failed to convert metadata string to dictionary: { error } " )
2879+ return {}
2880+
2881+ if not isinstance (parsed , dict ):
2882+ logger .warning ("metadata JSON is not an object; ignoring and starting fresh" )
2883+ return {}
2884+
2885+ return parsed
2886+
2887+
2888+ def dump_metadata (metadata_dict : Mapping [str , Any ]) -> Optional [str ]:
2889+ """
2890+ Serialize a metadata dictionary into a JSON string.
2891+
2892+ Args:
2893+ metadata_dict: Metadata dictionary to serialize.
2894+
2895+ Returns:
2896+ JSON string on success, otherwise None if serialization fails.
2897+ """
2898+ try :
2899+ return dumps (metadata_dict )
2900+ except Exception as error : # pragma: no cover (logger side-effect)
2901+ logger .warning (f"failed to convert metadata dictionary to string: { error } " )
2902+ return None
2903+
2904+
2905+ def merge_worker_maps (
2906+ metadata_dict : JsonObject ,
2907+ pilot_home : str ,
2908+ mappings : Tuple [Tuple [str , str ], ...],
2909+ read_json : ReadJsonFn ,
2910+ ) -> bool :
2911+ """
2912+ Merge worker node maps (worker + GPU) into the metadata dictionary.
2913+
2914+ Reads JSON files from `pilot_home` and merges them into `metadata_dict` under
2915+ the provided metadata keys. Only counts as a change if the new value differs
2916+ from the existing value.
2917+
2918+ Args:
2919+ metadata_dict: Metadata dictionary to update in-place.
2920+ pilot_home: Base directory where pilot map files are located.
2921+ mappings: Tuples of (filename, metadata_key) to read and merge.
2922+ read_json: Function that reads a JSON file and returns a mapping (or None).
2923+
2924+ Returns:
2925+ True if metadata_dict was modified, otherwise False.
2926+ """
2927+ changed = False
2928+
2929+ for fname , meta_key in mappings :
2930+ path = os .path .join (pilot_home , fname )
2931+ if not os .path .exists (path ):
2932+ continue
2933+
2934+ data = read_json (path )
2935+ if not data :
2936+ continue
2937+
2938+ # Ensure JSON-like (mapping) content.
2939+ if not isinstance (data , Mapping ):
2940+ logger .warning (f"map file does not contain a JSON object: { path } " )
2941+ continue
2942+
2943+ if metadata_dict .get (meta_key ) != data :
2944+ metadata_dict [meta_key ] = dict (data ) # make it JSON-serializable/mutable
2945+ changed = True
2946+
2947+ logger .info (f"added { meta_key } to metadata from { path } " )
2948+
2949+ return changed
2950+
2951+
28452952def update_server (job : Any , args : Any ) -> None :
28462953 """
28472954 Update the server (wrapper for send_state() that also prepares the metadata).
@@ -2856,11 +2963,39 @@ def update_server(job: Any, args: Any) -> None:
28562963 # user specific actions
28572964 pilot_user = os .environ .get ('PILOT_USER' , 'generic' ).lower ()
28582965 user = __import__ (f'pilot.user.{ pilot_user } .common' , globals (), locals (), [pilot_user ], 0 )
2859- metadata = user .get_metadata (job .workdir )
28602966 try :
28612967 user .update_server (job )
28622968 except Exception as error :
28632969 logger .warning ('exception caught in update_server(): %s' , error )
2970+
2971+ # the metadata can now be enhanced with the worker node map + GPU map for the case
2972+ # when the pilot is not sending the maps to the server directly. In this case, the maps
2973+ # are extracted on the server side at a later stage
2974+ # note: if the metadata does not exist, we should create it here
2975+
2976+ metadata : Optional [str ] = user .get_metadata (job .workdir )
2977+
2978+ if not args .update_server :
2979+ pilot_home : str = os .environ .get ("PILOT_HOME" , "" )
2980+ mappings : Tuple [Tuple [str , str ], ...] = (
2981+ (config .Workernode .map , "worker_node" ),
2982+ (config .Workernode .gpu_map , "worker_node_gpus" ),
2983+ )
2984+
2985+ metadata_dict : JsonObject = load_metadata_dict (metadata )
2986+ changed : bool = merge_worker_maps (
2987+ metadata_dict = metadata_dict ,
2988+ pilot_home = pilot_home ,
2989+ mappings = mappings ,
2990+ read_json = read_json ,
2991+ )
2992+
2993+ # Only dump if something changed, OR if metadata was missing and we added something.
2994+ if changed :
2995+ new_metadata = dump_metadata (metadata_dict )
2996+ if new_metadata is not None :
2997+ metadata = new_metadata
2998+
28642999 if job .fileinfo :
28653000 send_state (job , args , job .state , xml = dumps (job .fileinfo ), metadata = metadata )
28663001 else :
0 commit comments