File indexing completed on 2026-11-09 10:47:34
0001 """Payload parallelism of running production jobs, separate from allocation."""
0002 import re
0003 from collections import defaultdict
0004 from datetime import datetime, timezone
0005
0006 from django.db import connections, transaction
0007
0008 from .cached_product import get_product
0009 from .models import CachedProduct, EpicProdJob
0010 from .panda.queries import PANDA_SCHEMA
0011
0012 KEY = 'processing_activity:v1'
0013 TTL = 60
0014 ES_STALE_SECONDS = 600
0015 ES_EVENTS_WINDOW_S = 3600
0016
0017
0018 def payload_threads(corecount, actualcorecount, eventservice, metrics, record, now):
0019 """ES busy work lanes, or the native payload's selected thread width.
0020
0021 Each ES work lane runs one single-threaded payload. Native multicore
0022 allocations need the chosen payloadThreads report; allocation alone
0023 cannot establish that the payload uses those cores.
0024 """
0025 if eventservice:
0026 if not isinstance(record, dict) or not record.get('written_at'):
0027 return None
0028 if record.get('ended_at'):
0029 return 0
0030 age = now - float(record['written_at'])
0031 if not 0 <= age <= ES_STALE_SECONDS or 'in_flight' not in record:
0032 return None
0033 return len({unit['slot'] for unit in record['in_flight'] if 'slot' in unit})
0034 chosen = re.search(r'(?:^|\s)payloadThreads=(\d+)(?:\s|$)', metrics or '')
0035 if chosen:
0036 return int(chosen[1])
0037
0038
0039 if int(corecount or 1) == 1 and int(actualcorecount or 1) == 1:
0040 return 1
0041 return None
0042
0043
0044 def closed_events_since(record, since):
0045 """The events in an Event Service harness record's closes that stood
0046 (registered their output) at or after ``since``, epoch seconds: the
0047 queue's production in that window, since an ES job finishes once in
0048 hours and its work arrives close by close."""
0049 if not isinstance(record, dict):
0050 return 0
0051 return sum(int(c.get('events') or 0) for c in record.get('closes') or ()
0052 if isinstance(c, dict) and c.get('ok') and float(c.get('ended_at') or 0) >= since)
0053
0054
0055 def build():
0056 now = datetime.now(timezone.utc)
0057 sql = f'''SELECT pandaid, computingsite, COALESCE(destinationsite,''),
0058 corecount, actualcorecount, eventservice, jobmetrics
0059 FROM "{PANDA_SCHEMA}".jobsactive4
0060 WHERE jobstatus='running' AND workinggroup='EIC'
0061 AND (prodsourcelabel='managed' OR processingtype='epicproduction')
0062 AND COALESCE(processingtype,'') <> 'canary' '''
0063 with transaction.atomic(using='panda'):
0064 with connections['panda'].cursor() as cursor:
0065 cursor.execute("SET LOCAL statement_timeout = '20s'")
0066 cursor.execute(sql)
0067 rows = cursor.fetchall()
0068 records = dict(EpicProdJob.objects.filter(pandaid__in=[r[0] for r in rows if r[5]])
0069 .values_list('pandaid', 'data'))
0070 def empty():
0071 return {'known_threads': 0, 'jobs': 0, 'unknown_jobs': 0,
0072 'events_1h': 0, 'events_unknown_jobs': 0}
0073 queues, executors = defaultdict(empty), defaultdict(empty)
0074 for pid, queue, destination, cores, actual, es, metrics in rows:
0075 record = ((records.get(pid) or {}).get('es_record') or {}).get('record')
0076 threads = payload_threads(cores, actual, es, metrics, record, now.timestamp())
0077 for bucket in (queues[queue], executors[destination] if queue.startswith('BNL_OSG') else None):
0078 if bucket is None:
0079 continue
0080 bucket['jobs'] += 1
0081 bucket['unknown_jobs'] += threads is None
0082 bucket['known_threads'] += threads or 0
0083
0084
0085
0086 since = now.timestamp() - ES_EVENTS_WINDOW_S
0087 ended_sql = f'''SELECT pandaid, computingsite FROM "{PANDA_SCHEMA}".jobsarchived4
0088 WHERE endtime >= %s AND workinggroup='EIC' AND COALESCE(eventservice, 0) <> 0
0089 AND (prodsourcelabel='managed' OR processingtype='epicproduction')
0090 AND COALESCE(processingtype,'') <> 'canary' '''
0091 with transaction.atomic(using='panda'):
0092 with connections['panda'].cursor() as cursor:
0093 cursor.execute("SET LOCAL statement_timeout = '20s'")
0094 cursor.execute(ended_sql, [datetime.fromtimestamp(since, timezone.utc).replace(tzinfo=None)])
0095 ended = cursor.fetchall()
0096 es_jobs = [(pid, queue) for pid, queue, _d, _c, _a, es, _m in rows if es] + list(ended)
0097 missing = [pid for pid, _q in es_jobs if pid not in records]
0098 if missing:
0099 records.update(EpicProdJob.objects.filter(pandaid__in=missing).values_list('pandaid', 'data'))
0100 for pid, queue in es_jobs:
0101 record = ((records.get(pid) or {}).get('es_record') or {}).get('record')
0102 if isinstance(record, dict):
0103 queues[queue]['events_1h'] += closed_events_since(record, since)
0104 else:
0105 queues[queue]['events_unknown_jobs'] += 1
0106
0107
0108
0109
0110
0111 previous = (CachedProduct.objects.filter(key=KEY)
0112 .values_list('value', flat=True).first()) or {}
0113 for scope, buckets in (('queues', queues), ('executors', executors)):
0114 held = previous.get(scope) or {}
0115 for name, bucket in buckets.items():
0116 if not bucket['unknown_jobs']:
0117 bucket['threads'], bucket['threads_at'] = bucket['known_threads'], now.isoformat()
0118 continue
0119 last = held.get(name) or {}
0120 if (last.get('threads') is not None and last.get('threads_at')
0121 and last['threads'] > bucket['known_threads']):
0122 bucket['threads'], bucket['threads_at'] = last['threads'], last['threads_at']
0123 else:
0124 bucket['threads'], bucket['threads_at'] = bucket['known_threads'], None
0125 return {'queues': dict(queues), 'executors': dict(executors), 'observed_at': now.isoformat()}
0126
0127
0128 def product(refresh=False):
0129 return get_product(KEY, build, TTL, refresh=refresh, async_first_fill=True)