File indexing completed on 2026-09-28 09:36:48
0001 """
0002 Offline check of insert_step_task, the DEFT insert used for a workflow step's task.
0003
0004 Drives the real WorkflowModule.insert_step_task against a fake cursor, so the SQL it builds, the
0005 late-bound ${TASKID} resolution, the active-task throttle and both sequence backends are exercised
0006 without a database.
0007
0008 Run from the repository root: python3 pandaserver/workflow/examples/insert_step_task_test.py
0009 """
0010
0011 import json
0012 import os
0013 import sys
0014 import types
0015 from typing import Any, cast
0016
0017 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
0018
0019
0020 def stub(n, **a):
0021 m = types.ModuleType(n)
0022 for k, v in a.items():
0023 setattr(m, k, v)
0024 sys.modules[n] = m
0025 return m
0026
0027
0028 stub("pandacommon")
0029 pu = stub("pandacommon.pandautils")
0030 pu.__path__ = []
0031 stub("pandacommon.pandautils.base", SpecBase=object)
0032
0033
0034 def get_sql_IN_bind_variables(vals, prefix=":", value_as_suffix=False):
0035 names = [f"{prefix}{v}" for v in vals]
0036 return ",".join(names), {n: v for n, v in zip(names, vals)}
0037
0038
0039 stub("pandacommon.pandautils.PandaUtils", get_sql_IN_bind_variables=get_sql_IN_bind_variables, naive_utcnow=lambda: None)
0040 pl = stub("pandacommon.pandalogger")
0041 pl.__path__ = []
0042
0043
0044 class Log:
0045 def __init__(s, *a, **k):
0046 s.msgs = []
0047
0048 def info(s, m):
0049 pass
0050
0051 def debug(s, m):
0052 s.msgs.append(m)
0053
0054 def warning(s, m):
0055 pass
0056
0057 def error(s, m):
0058 s.msgs.append("ERR:" + str(m))
0059
0060
0061 stub("pandacommon.pandalogger.LogWrapper", LogWrapper=Log)
0062 stub("pandacommon.pandalogger.PandaLogger", PandaLogger=lambda: types.SimpleNamespace(getLogger=lambda n: None))
0063 stub("pandaserver.config", panda_config=types.SimpleNamespace(schemaJEDI="ATLAS_PANDA", schemaDEFT="ATLAS_DEFT"))
0064 stub("pandaserver.srvcore")
0065 stub("pandaserver.srvcore.CoreUtils", clean_user_id=lambda d: "tester")
0066 stub("pandaserver.taskbuffer.ErrorCode")
0067 stub("pandaserver.taskbuffer.JobUtils")
0068 stub("pandaserver.taskbuffer.JobSpec", JobSpec=object)
0069
0070
0071 class BaseModule:
0072 _max: int | None = None
0073
0074 def __init__(self, log_stream=None):
0075 self.backend = "oracle"
0076
0077 def create_tagged_logger(self, comment, tag=""):
0078 return Log()
0079
0080 def getvalue_corrector(self, v):
0081 return v
0082
0083 def getConfigValue(self, comp, key, app=None, vo=None):
0084 return self._max
0085
0086 def dump_error_message(self, log):
0087 pass
0088
0089 def _commit(self):
0090 return True
0091
0092 def _rollback(self):
0093 pass
0094
0095
0096 stub("pandaserver.taskbuffer.db_proxy_mods.base_module", BaseModule=BaseModule, varNUMBER=int)
0097 stub("pandaserver.taskbuffer.db_proxy_mods.entity_module", get_entity_module=lambda s: None)
0098
0099 import contextlib
0100
0101 from pandaserver.taskbuffer.db_proxy_mods.workflow_module import WorkflowModule
0102 from pandaserver.workflow.workflow_base import TASKID_PLACEHOLDER
0103
0104
0105 class Cur:
0106 def __init__(s, task_id, active_count):
0107 s.task_id = task_id
0108 s.active_count = active_count
0109 s.executed = []
0110 s._last = None
0111
0112 def var(s, t):
0113 return "OUT"
0114
0115 def execute(s, sql, varmap=None):
0116 s.executed.append((sql, dict(varmap or {})))
0117 if "COUNT(*)" in sql:
0118 s._last = (s.active_count,)
0119
0120 def fetchone(s):
0121 return s._last
0122
0123 def getvalue(s, ref):
0124 return s.task_id
0125
0126
0127 class Proxy(WorkflowModule):
0128 def __init__(s, task_id=49900001, active_count=0, max_tasks=None):
0129 s.backend = "oracle"
0130
0131
0132
0133 s.fake_cur = Cur(task_id, active_count)
0134 s.cur = cast(Any, s.fake_cur)
0135 s._max = max_tasks
0136
0137 def create_tagged_logger(s, comment, tag=""):
0138 return Log()
0139
0140 def getvalue_corrector(s, v):
0141 return v
0142
0143 def getConfigValue(s, comp, key, app=None, vo=None):
0144 return s._max
0145
0146 def dump_error_message(s, log):
0147 pass
0148
0149 def _commit(s):
0150 return True
0151
0152 def _rollback(s, useOtherError=False):
0153 return True
0154
0155 @contextlib.contextmanager
0156 def transaction(s, name=None, tmp_log=None):
0157 yield (s.cur, tmp_log or Log())
0158
0159
0160 FAILURES: list[str] = []
0161
0162
0163 def ok(label, condition, detail=""):
0164 if not condition:
0165 FAILURES.append(label)
0166 print(f" {'PASS' if condition else 'FAIL'} {label}{' ' + str(detail) if detail and not condition else ''}")
0167
0168
0169 base = {
0170 "taskName": "mc23.evgen.e8590",
0171 "vo": "atlas",
0172 "prodSourceLabel": "managed",
0173 "userName": "mnegrini",
0174 "taskPriority": 275,
0175 "jobParameters": [
0176 {
0177 "type": "template",
0178 "param_type": "output",
0179 "dataset": f"mc23.evgen.EVNT.e8590_wfid12345_tid{TASKID_PLACEHOLDER}_00",
0180 "value": f"--outputEVNTFile=EVNT.{TASKID_PLACEHOLDER}._${{SN}}.pool.root",
0181 }
0182 ],
0183 }
0184
0185 print("insert_step_task:")
0186 p = Proxy()
0187 tid, msg = p.insert_step_task(dict(base), "/DC=ch/CN=t")
0188 ok("returns the new task id", tid == 49900001, (tid, msg))
0189 sqls = [e[0] for e in p.fake_cur.executed]
0190 ok("one INSERT into T_TASK", sum("INSERT INTO ATLAS_DEFT.T_TASK" in q for q in sqls) == 1, sqls)
0191 ok("followed by the placeholder UPDATE", sum("UPDATE ATLAS_DEFT.T_TASK SET jedi_task_parameters" in q for q in sqls) == 1)
0192 ins = [e for e in p.fake_cur.executed if "INSERT INTO ATLAS_DEFT.T_TASK" in e[0]][0]
0193 ok("uses the sequence for taskid", "PRODSYS2_TASK_ID_SEQ.nextval" in ins[0])
0194 ok("own id as parent when no parent given", "PRODSYS2_TASK_ID_SEQ.currval" in ins[0])
0195 ok("priority carried from taskPriority", ins[1][":priority"] == 275 and ins[1][":current_priority"] == 275)
0196 ok("userName taken as authored, not the DN", ins[1][":userName"] == "mnegrini")
0197 ok("status queued as waiting", ins[1][":status"] == "waiting")
0198 ok("inserted params still hold the placeholder", TASKID_PLACEHOLDER in ins[1][":param"])
0199 upd = [e for e in p.fake_cur.executed if "UPDATE" in e[0]][0]
0200 ok("update resolves it to the real id", TASKID_PLACEHOLDER not in upd[1][":param"] and "tid49900001_00" in upd[1][":param"])
0201 ok("update also resolves it in the LFN template", "EVNT.49900001._${SN}" in upd[1][":param"])
0202 ok("update targets the new task", upd[1][":taskid"] == 49900001)
0203
0204 print("\nno placeholder -> no extra UPDATE:")
0205 clean = json.loads(json.dumps(base).replace(TASKID_PLACEHOLDER, "48810693"))
0206 p2 = Proxy()
0207 tid2, _ = p2.insert_step_task(clean, "/DC=ch/CN=t")
0208 ok("still inserts", tid2 == 49900001)
0209 ok("no UPDATE issued", not any("UPDATE" in q for q, _ in p2.fake_cur.executed))
0210
0211 print("\nexplicit parent_tid:")
0212 p3 = Proxy()
0213 p3.insert_step_task(dict(base), "/DC=ch/CN=t", parent_tid=48810693)
0214 ins3 = [e for e in p3.fake_cur.executed if "INSERT INTO" in e[0]][0]
0215 ok("binds the given parent", ins3[1][":parent_tid"] == 48810693 and "currval" not in ins3[0])
0216
0217 print("\nactive task throttle:")
0218 p4 = Proxy(active_count=500, max_tasks=100)
0219 tid4, msg4 = p4.insert_step_task(dict(base), "/DC=ch/CN=t")
0220 ok("refused when over the limit", tid4 is None)
0221 ok("message explains it", "too many active tasks" in msg4, msg4)
0222 ok("nothing inserted", not any("INSERT INTO ATLAS_DEFT.T_TASK" in q for q, _ in p4.fake_cur.executed))
0223 p5 = Proxy(active_count=50, max_tasks=100)
0224 tid5, _ = p5.insert_step_task(dict(base), "/DC=ch/CN=t")
0225 ok("allowed when under the limit", tid5 == 49900001)
0226 p6 = Proxy(active_count=999, max_tasks=None)
0227 tid6, _ = p6.insert_step_task(dict(base), "/DC=ch/CN=t")
0228 ok("no limit configured -> no throttle query", tid6 == 49900001 and not any("COUNT(*)" in q for q, _ in p6.fake_cur.executed))
0229
0230 print("\nmissing required params:")
0231 for key in ["taskName", "vo", "prodSourceLabel", "userName"]:
0232 bad = dict(base)
0233 del bad[key]
0234 t, m = Proxy().insert_step_task(bad, "/DC=ch/CN=t")
0235 ok(f"rejects missing {key}", t is None and key in m, m)
0236
0237 print("\nmysql-style backend (no sequence):")
0238
0239
0240 class MyProxy(Proxy):
0241 def __init__(s, **k):
0242 super().__init__(**k)
0243 s.backend = "mysql"
0244
0245
0246 class MyCur(Cur):
0247 def execute(s, sql, varmap=None):
0248 s.executed.append((sql, dict(varmap or {})))
0249 if "COUNT(*)" in sql:
0250 s._last = (s.active_count,)
0251 elif "LAST_INSERT_ID" in sql:
0252 s._last = (49900001,)
0253
0254
0255 p7 = MyProxy()
0256 p7.fake_cur = MyCur(49900001, 0)
0257 p7.cur = cast(Any, p7.fake_cur)
0258 tid7, msg7 = p7.insert_step_task(dict(base), "/DC=ch/CN=t")
0259 ok("inserts via the stand-in sequence table", tid7 == 49900001, msg7)
0260 q7 = [e for e in p7.fake_cur.executed if "INSERT INTO ATLAS_DEFT.T_TASK" in e[0]][0]
0261 ok("binds nextval instead of a sequence", q7[1].get(":nextval") == 49900001 and "PRODSYS2_TASK_ID_SEQ.nextval" not in q7[0])
0262 ok("own id as parent uses the same value", ":nextval" in q7[0].split("VALUES")[1] and "currval" not in q7[0])
0263
0264 print(f"\n{'ALL CHECKS PASSED' if not FAILURES else f'{len(FAILURES)} CHECK(S) FAILED'}")
0265 sys.exit(1 if FAILURES else 0)