diff --git a/ACT/LibMCData.xml b/ACT/LibMCData.xml
index dc5583b37..c0a417087 100644
--- a/ACT/LibMCData.xml
+++ b/ACT/LibMCData.xml
@@ -1415,6 +1415,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DevTools/executionlist_perf/README.md b/DevTools/executionlist_perf/README.md
new file mode 100644
index 000000000..67f2908d7
--- /dev/null
+++ b/DevTools/executionlist_perf/README.md
@@ -0,0 +1,72 @@
+# Execution list performance
+
+`executionlist_perf.py` seeds a large build history into a dev `amc_server` and measures the
+Build History list, for before/after comparisons of changes to it. It needs only
+Python 3 (standard library) on Linux.
+
+## Usage
+
+```bash
+python3 DevTools/executionlist_perf/executionlist_perf.py run \
+ --output build_linux64/Output --workdir /tmp/elperf --fresh [-n 2000] [--json result.json]
+```
+
+`run` does every step and then stops the server it started:
+
+1. **prepare**: creates `--workdir` from the build's `Output/`. The binaries are symlinked. The
+ MinimalSystem config is copied with a `buildhistory` page added (an `executionlist`, as in
+ `Examples/LPBFSystem/config.xml`). The server binds to 127.0.0.1:18869 (`--port`) with
+ `telemetry="off"`.
+2. Starts the server once so that it creates and migrates `data/storage.db`. This step is
+ skipped when the database already exists.
+3. **seed**: writes N jobs and executions straight into SQLite. The seed is deterministic, and
+ rerunning it replaces its own rows. About 85% of executions are finished, 10% failed and 5%
+ in process. Every 10th job is `deleted`, and every 3rd job has a thumbnail UUID. Every seeded
+ row gets an `incremental_id` (one step per row, above any existing rows), so the list head
+ is the newest seeded execution and `?since=` returns exactly the last k rows.
+4. Starts the server and **measures** each mode in `--modes` (default
+ `full,concurrent,delta,frontend`). It logs in as `test`/`test` with the same
+ `/api/auth` session-key handshake the client uses, and sends the token as a Bearer header.
+
+The steps can also run one at a time, against a server you start yourself:
+
+```bash
+executionlist_perf.py prepare --output build_linux64/Output --workdir W
+executionlist_perf.py seed W/data/storage.db -n 2000 # server stopped
+executionlist_perf.py measure --pid [--modes full,delta --single-count 20]
+```
+
+## Modes
+
+The client (`Client/core/modules/AMCModule_ExecutionList.js` with `AMCListDeltaSync`) fetches
+the full list once when the page loads, then only a `?since=` delta whenever the
+`executionlistheadid` attribute changes. It does not poll the list.
+
+| mode | what it simulates |
+|--------------|-------------------|
+| `full` | Sequential `GET /api/executions` (`--single-count`): one page load. |
+| `concurrent` | `--clients` (default 6) full fetches started at once: several tabs or devices loading together. |
+| `delta` | Sequential `GET /api/executions?since=` for each k in `--delta-sizes` (default `1,10,100`): the fetch after k executions changed. |
+| `frontend` | Sequential `GET /api/frontend?pages=buildhistory`: the scoped v2 snapshot the client polls when it has no SSE stream. The list item contributes only its head id, so this should not grow with N. |
+
+## Output
+
+- `latency_*_s`: per-request latency.
+- `rows`, `response_bytes`: list entries and JSON size of one response.
+- `wall_s` (concurrent): time until the last of the simultaneous fetches finished.
+- `errors` / `first_error`: failed requests and the server's message for the first.
+- `cpu.cpu_mean_pct` / `cpu.cpu_peak_pct`: `amc_server` CPU from `/proc//stat`, sampled
+ every 1 s over the mode. 100% means one core. Short modes finish before the first sample, so
+ only the mean is meaningful there.
+
+Measure an optimised build when you want device-like numbers. An empty `CMAKE_BUILD_TYPE`
+compiles at -O0, while device builds use `-DCMAKE_BUILD_TYPE=Release`. Compare before/after
+only between builds of the same type.
+
+## Damaged databases
+
+`--damaged K` (on `seed` and `run`) appends K executions whose job row does not exist, as in a
+damaged database. They carry the newest `incremental_id`s, so they are in the full list and in
+every delta. Before the per-entry degrade fix in LibMCData, one such row makes the whole
+`/api/executions` request fail (`errors` > 0), and the client's list stays frozen.
+
diff --git a/DevTools/executionlist_perf/executionlist_perf.py b/DevTools/executionlist_perf/executionlist_perf.py
new file mode 100755
index 000000000..ddcfb4eb0
--- /dev/null
+++ b/DevTools/executionlist_perf/executionlist_perf.py
@@ -0,0 +1,425 @@
+#!/usr/bin/env python3
+"""Seed a large build history into a dev amc_server and measure the Build History list.
+
+Before/after tooling for the execution list (N+1 queries per list request). Standard
+library only. See README.md next to this file.
+
+Subcommands
+ prepare Build a scratch server directory from a build Output dir (MinimalSystem plus a
+ "buildhistory" page with an executionlist), bound to 127.0.0.1, telemetry off.
+ seed Insert N build jobs + executions straight into storage.db (server stopped).
+ measure Fetch the execution list of a running server the way the client does and report
+ latency, response size and amc_server CPU.
+ run prepare + start/stop once to migrate the DB + seed + start + measure all modes + stop.
+
+Measure modes (measure --modes, run --modes)
+ full Sequential GET /api/executions: the full list a client fetches once per page load
+ (Client/core/modules/AMCModule_ExecutionList.js via AMCListDeltaSync).
+ concurrent --clients full fetches started at once: several tabs/devices loading together.
+ delta Sequential GET /api/executions?since= for each k in --delta-sizes: the
+ incremental fetch after k executions changed (the client sends one per head change).
+ frontend Sequential GET /api/frontend?pages=buildhistory: the scoped v2 snapshot the
+ client polls when it has no SSE stream (the list item only adds its head id).
+"""
+import argparse, hashlib, json, os, random, re, shutil, signal, socket, sqlite3, subprocess
+import sys, threading, time, urllib.error, urllib.parse, urllib.request, uuid
+from datetime import datetime, timedelta, timezone
+
+DEFAULT_PORT = 18869
+PAGE = "buildhistory"
+
+# --------------------------------------------------------------------------------------------
+# prepare
+
+BUILDHISTORY_PAGE = """
+
+
+
+
+
+
+
+"""
+BUILDHISTORY_MENU = """
+"""
+
+
+def prepare(output, workdir, port):
+ output, workdir = os.path.abspath(output), os.path.abspath(workdir)
+ server_xml = open(os.path.join(output, "amc_server.xml")).read()
+ githash = re.search(r'githash="([0-9a-f]+)"', server_xml).group(1)
+ os.makedirs(os.path.join(workdir, "data"), exist_ok=True)
+ for name in os.listdir(output):
+ if not name.startswith(githash + "_") or name.endswith("_config.xml"):
+ continue
+ link = os.path.join(workdir, name)
+ if os.path.lexists(link):
+ os.remove(link)
+ os.symlink(os.path.join(output, name), link)
+
+ config = open(os.path.join(output, githash + "_config.xml")).read()
+ if 'name="buildhistory"' not in config:
+ # Insert the page before the menu and a menu entry at the start of the menu.
+ config, n = re.subn(r"(\n\s*\s*\n)", lambda m: "\n" + BUILDHISTORY_PAGE + m.group(1) + BUILDHISTORY_MENU,
+ config, count=1)
+ if n != 1:
+ sys.exit("prepare: no in %s_config.xml; is this the MinimalSystem config?" % githash)
+ open(os.path.join(workdir, githash + "_config.xml"), "w").write(config)
+
+ server_xml = re.sub(r']*/>', ' ' % port, server_xml)
+ server_xml = re.sub(r']*?)\s*(telemetry="[^"]*")?\s*/>',
+ lambda m: ' ' % m.group(1), server_xml)
+ open(os.path.join(workdir, "amc_server.xml"), "w").write(server_xml)
+ return os.path.join(output, "amc_server")
+
+
+# --------------------------------------------------------------------------------------------
+# seed
+
+SEED_NS = uuid.UUID("5eed0000-0000-4000-8000-000000000000")
+EMPTY_UUID = "00000000-0000-0000-0000-000000000000"
+
+
+def seed(db, n, damaged=0):
+ """Deterministic and idempotent: removes its own rows (seed-% / seed:%) first.
+
+ Mix: ~85% finished, 10% failed, 5% inprocess (from a fake past journal); every 10th job
+ has status 'deleted' (a row, as DeleteJob leaves it); every 3rd job has a
+ thumbnailuuid (pointing at no stream, so the browser shows a broken image; the list JSON
+ is unaffected).
+
+ incremental_id is set the way bumpIncrementalID would have left it: one step per row, in
+ seed order, on top of whatever non-seed rows hold. The list head therefore equals the
+ newest seeded execution and ?since= returns exactly the last k rows.
+
+ damaged > 0 appends that many executions whose job row does not exist (a damaged
+ database). They get the newest incremental ids, so both the full list and every delta
+ include them.
+ """
+ u = lambda kind, i: str(uuid.uuid5(SEED_NS, "%s-%d" % (kind, i))) # lowercase, hyphenated
+ ts = lambda d: d.strftime("%Y-%m-%dT%H:%M:%S.%fZ") # convertToISO8601TimeUTC
+ rnd = random.Random(42)
+ base = datetime(2025, 1, 1, tzinfo=timezone.utc)
+ journal = u("journal", 0)
+ con = sqlite3.connect(db)
+ c = con.cursor()
+ c.execute("DELETE FROM buildjobexecutions WHERE description LIKE 'seed:%'")
+ c.execute("DELETE FROM buildjobs WHERE name LIKE 'seed-%'")
+ job_head = c.execute("SELECT COALESCE(MAX(incremental_id), 0) FROM buildjobs").fetchone()[0]
+ exec_head = c.execute("SELECT COALESCE(MAX(incremental_id), 0) FROM buildjobexecutions").fetchone()[0]
+
+ def add_execution(i, exe, job, start, est, end, description):
+ c.execute("INSERT INTO buildjobexecutions (uuid,jobuuid,journaluuid,startjournaltimestamp,endjournaltimestamp,useruuid,"
+ "status,description,active,timestamp,incremental_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
+ (exe, job, journal, ts(start), ts(end) if est != "inprocess" else 0, EMPTY_UUID, est,
+ description, 1, ts(start), exec_head + i + 1))
+
+ for i in range(n):
+ job, exe = u("job", i), u("exec", i)
+ start = base + timedelta(minutes=37 * i)
+ end = start + timedelta(seconds=rnd.randint(60, 7200))
+ jstatus = "deleted" if i % 10 == 9 else "validated"
+ thumb = u("thumb", i) if i % 3 == 0 else EMPTY_UUID
+ c.execute("INSERT INTO buildjobs (uuid,name,status,storagestreamuuid,layercount,userid,timestamp,useruuid,thumbnailuuid,"
+ "incremental_id) VALUES (?,?,?,?,?,?,?,?,?,?)",
+ (job, "seed-job-%05d" % i, jstatus, u("stream", i), rnd.randint(10, 500), "test", ts(start), EMPTY_UUID, thumb,
+ job_head + i + 1))
+ r = rnd.random()
+ est = "finished" if r < .85 else ("failed" if r < .95 else "inprocess")
+ add_execution(i, exe, job, start, est, end, "seed: execution %d" % i)
+ for d in range(damaged):
+ start = base + timedelta(minutes=37 * (n + d))
+ add_execution(n + d, u("damagedexec", d), u("missingjob", d), start, "finished", start + timedelta(minutes=10),
+ "seed: damaged execution %d (job row missing)" % d)
+ con.commit()
+ count = c.execute("SELECT count(*) FROM buildjobexecutions WHERE active=1").fetchone()[0]
+ head = c.execute("SELECT COALESCE(MAX(incremental_id), 0) FROM buildjobexecutions").fetchone()[0]
+ con.close()
+ return count, head
+
+
+# --------------------------------------------------------------------------------------------
+# HTTP client
+
+class Api:
+ def __init__(self, base, user="test", password="test"):
+ self.base = base.rstrip("/")
+ H = lambda s: hashlib.sha256(s.encode()).hexdigest()
+ s = self._req("/auth/", {"username": user})
+ ck = s["sessionkey"]
+ salted = H(s["loginsalt"] + password)
+ self.token = self._req("/auth/" + s["sessionuuid"],
+ {"clientkey": ck, "password": H(s["sessionkey"] + H(ck + salted))})["token"]
+
+ def _req(self, path, data=None, timeout=600, raw=False):
+ headers = {"Content-Type": "application/json"}
+ if getattr(self, "token", None):
+ headers["Authorization"] = "Bearer " + self.token
+ r = urllib.request.Request(self.base + path, headers=headers,
+ data=json.dumps(data).encode() if data is not None else None)
+ with urllib.request.urlopen(r, timeout=timeout) as resp:
+ body = resp.read()
+ return body if raw else json.loads(body)
+
+ def get(self, path):
+ """GET returning (parsed JSON, response bytes). HTTP errors raise ApiError with the server's message."""
+ try:
+ body = self._req(path, raw=True)
+ except urllib.error.HTTPError as e:
+ raise ApiError("HTTP %d: %s" % (e.code, e.read()[:300].decode(errors="replace")))
+ return json.loads(body), len(body)
+
+ def executions(self, since=None):
+ return self.get("/executions" + ("" if since is None else "?since=%d" % since))
+
+ def frontend(self, page):
+ return self.get("/frontend?" + urllib.parse.urlencode({"pages": page, "dialogs": ""}))
+
+
+class ApiError(Exception):
+ pass
+
+
+# --------------------------------------------------------------------------------------------
+# CPU sampling from /proc (no sysstat needed)
+
+class CpuSampler(threading.Thread):
+ def __init__(self, pid, period=1.0):
+ super().__init__(daemon=True)
+ self.pid, self.period, self.samples = pid, period, []
+ self.tick = os.sysconf("SC_CLK_TCK")
+ self._halt = threading.Event()
+
+ def _cpu_seconds(self):
+ f = open("/proc/%d/stat" % self.pid).read().rsplit(")", 1)[1].split()
+ return (int(f[11]) + int(f[12])) / self.tick # utime + stime
+
+ def run(self):
+ self.t0, self.c0 = time.monotonic(), self._cpu_seconds()
+ last_t, last_c = self.t0, self.c0
+ while not self._halt.wait(self.period):
+ t, c = time.monotonic(), self._cpu_seconds()
+ self.samples.append(100.0 * (c - last_c) / (t - last_t))
+ last_t, last_c = t, c
+ self.t1, self.c1 = time.monotonic(), self._cpu_seconds()
+
+ def stop(self):
+ self._halt.set()
+ self.join()
+ return {"cpu_mean_pct": round(100.0 * (self.c1 - self.c0) / (self.t1 - self.t0), 1),
+ "cpu_peak_pct": round(max(self.samples), 1) if self.samples else None}
+
+
+# --------------------------------------------------------------------------------------------
+# measure
+
+def pct(values, p):
+ v = sorted(values)
+ return v[min(len(v) - 1, int(round(p / 100.0 * (len(v) - 1))))] if v else None
+
+
+def latency_stats(lat):
+ return {"requests": len(lat), "latency_min_s": round(min(lat), 4) if lat else None,
+ "latency_median_s": round(pct(lat, 50), 4) if lat else None,
+ "latency_max_s": round(max(lat), 4) if lat else None}
+
+
+def timed(fn, *args):
+ """(seconds, result, error) for one request; errors are reported, not raised."""
+ t = time.monotonic()
+ try:
+ res, err = fn(*args), None
+ except Exception as e: # ApiError, timeouts, connection resets
+ res, err = None, str(e)
+ return time.monotonic() - t, res, err
+
+
+def sequential(fn, count, *args):
+ lat, rows, size, errors = [], None, None, []
+ for _ in range(count):
+ dt, res, err = timed(fn, *args)
+ if err:
+ errors.append(err)
+ continue
+ lat.append(dt)
+ data, size = res
+ if rows is None and "executions" in data:
+ rows = len(data["executions"])
+ r = dict(latency_stats(lat), response_bytes=size, errors=len(errors))
+ if rows is not None:
+ r["rows"] = rows
+ if errors:
+ r["first_error"] = errors[0]
+ return r
+
+
+def measure_concurrent(api, clients):
+ lat, errors, lock = [], [], threading.Lock()
+
+ def one():
+ dt, _, err = timed(api.executions)
+ with lock:
+ (errors.append(err) if err else lat.append(dt))
+
+ t = time.monotonic()
+ threads = [threading.Thread(target=one, daemon=True) for _ in range(clients)]
+ for th in threads:
+ th.start()
+ for th in threads:
+ th.join()
+ r = dict(latency_stats(lat), clients=clients, wall_s=round(time.monotonic() - t, 4), errors=len(errors))
+ if errors:
+ r["first_error"] = errors[0]
+ return r
+
+
+def measure(base, pid, modes, single_count, clients, delta_sizes, page=PAGE):
+ api = Api(base)
+ try:
+ head = api.executions(0)[0]["executionlistheadid"] # since=0 is a delta of the whole list; only the head is needed
+ except ApiError:
+ head = None # a damaged list fails here too; each mode reports the error itself
+ results = {"page": page, "head_id": head, "modes": {}}
+ for mode in modes:
+ sampler = CpuSampler(pid) if pid else None
+ if sampler:
+ sampler.start()
+ if mode == "full":
+ r = sequential(api.executions, single_count)
+ elif mode == "concurrent":
+ r = measure_concurrent(api, clients)
+ elif mode == "delta":
+ r = {}
+ for k in delta_sizes:
+ r["k=%d" % k] = sequential(api.executions, single_count, max(0, (head or 0) - k))
+ elif mode == "frontend":
+ r = sequential(api.frontend, single_count, page)
+ else:
+ sys.exit("unknown mode %r" % mode)
+ if sampler:
+ r["cpu"] = sampler.stop()
+ results["modes"][mode] = r
+ print("%-10s %s" % (mode, json.dumps(r)), file=sys.stderr, flush=True)
+ return results
+
+
+# --------------------------------------------------------------------------------------------
+# run: everything end to end
+
+def wait_port(port, timeout=60):
+ deadline = time.monotonic() + timeout
+ while time.monotonic() < deadline:
+ try:
+ socket.create_connection(("127.0.0.1", port), 0.5).close()
+ return
+ except OSError:
+ time.sleep(0.2)
+ sys.exit("amc_server did not open port %d within %ds" % (port, timeout))
+
+
+class Server:
+ def __init__(self, binary, workdir, port):
+ self.log = open(os.path.join(workdir, "amc_server.log"), "ab")
+ self.proc = subprocess.Popen([binary], cwd=workdir, stdout=self.log, stderr=subprocess.STDOUT,
+ stdin=subprocess.DEVNULL, start_new_session=True)
+ wait_port(port)
+
+ def stop(self):
+ if self.proc.poll() is None:
+ os.killpg(self.proc.pid, signal.SIGTERM)
+ try:
+ self.proc.wait(10)
+ except subprocess.TimeoutExpired:
+ os.killpg(self.proc.pid, signal.SIGKILL)
+ self.proc.wait()
+ self.log.close()
+ # Journals are named journal_.db; a restart within the same second
+ # reuses the file and fails with "table `logs` already exists".
+ time.sleep(1.1)
+
+
+def run(args):
+ if args.fresh and os.path.isdir(args.workdir):
+ shutil.rmtree(args.workdir)
+ binary = prepare(args.output, args.workdir, args.port)
+ db = os.path.join(args.workdir, "data", "storage.db")
+ if not os.path.exists(db): # first start creates and migrates the schema
+ Server(binary, args.workdir, args.port).stop()
+ print("seeded: %d active executions, head id %d" % seed(db, args.n, args.damaged), file=sys.stderr)
+ srv = Server(binary, args.workdir, args.port)
+ try:
+ res = measure("http://127.0.0.1:%d/api" % args.port, srv.proc.pid, args.modes.split(","), args.single_count,
+ args.clients, delta_sizes(args.delta_sizes))
+ finally:
+ srv.stop()
+ res["executions"] = args.n
+ res["damaged"] = args.damaged
+ res["build_output"] = os.path.abspath(args.output)
+ return res
+
+
+# --------------------------------------------------------------------------------------------
+
+def delta_sizes(text):
+ return [int(k) for k in text.split(",") if k.strip()]
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ sub = ap.add_subparsers(dest="cmd", required=True)
+
+ def measure_opts(p):
+ p.add_argument("--modes", default="full,concurrent,delta,frontend",
+ help="comma list of full,concurrent,delta,frontend (default: all)")
+ p.add_argument("--single-count", type=int, default=5, help="requests per sequential measurement (default 5)")
+ p.add_argument("--clients", type=int, default=6, help="simultaneous full fetches in concurrent mode (default 6)")
+ p.add_argument("--delta-sizes", default="1,10,100", help="k values for delta mode: rows changed (default 1,10,100)")
+ p.add_argument("--json", help="also write the results to this file")
+
+ p = sub.add_parser("prepare", help="create a scratch server dir")
+ p.add_argument("--output", required=True, help="build Output dir, e.g. build_linux64/Output")
+ p.add_argument("--workdir", required=True)
+ p.add_argument("--port", type=int, default=DEFAULT_PORT)
+
+ p = sub.add_parser("seed", help="seed executions into storage.db (stop the server first)")
+ p.add_argument("db")
+ p.add_argument("-n", type=int, default=2000)
+ p.add_argument("--damaged", type=int, default=0, help="extra executions whose job row is missing (default 0)")
+
+ p = sub.add_parser("measure", help="measure against a running server")
+ p.add_argument("--base", default="http://127.0.0.1:%d/api" % DEFAULT_PORT)
+ p.add_argument("--pid", type=int, help="amc_server pid for CPU sampling")
+ p.add_argument("--page", default=PAGE)
+ measure_opts(p)
+
+ p = sub.add_parser("run", help="prepare, seed, start, measure, stop")
+ p.add_argument("--output", required=True, help="build Output dir, e.g. build_linux64/Output")
+ p.add_argument("--workdir", required=True, help="scratch dir for the server (data/ lives here)")
+ p.add_argument("--port", type=int, default=DEFAULT_PORT)
+ p.add_argument("-n", type=int, default=2000, help="executions to seed (default 2000)")
+ p.add_argument("--damaged", type=int, default=0, help="extra executions whose job row is missing (default 0)")
+ p.add_argument("--fresh", action="store_true", help="delete --workdir first")
+ measure_opts(p)
+
+ args = ap.parse_args()
+ if args.cmd == "prepare":
+ prepare(args.output, args.workdir, args.port)
+ print("prepared %s; start the server from there: (cd %s && %s/amc_server)"
+ % (args.workdir, args.workdir, os.path.abspath(args.output)))
+ return
+ if args.cmd == "seed":
+ print("%d active executions, head id %d" % seed(args.db, args.n, args.damaged))
+ return
+ if args.cmd == "measure":
+ res = measure(args.base, args.pid, args.modes.split(","), args.single_count, args.clients,
+ delta_sizes(args.delta_sizes), args.page)
+ else:
+ res = run(args)
+ out = json.dumps(res, indent=2)
+ print(out)
+ if args.json:
+ open(args.json, "w").write(out + "\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.h b/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.h
index d8b18fdf3..f5e65b974 100644
--- a/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.h
+++ b/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.h
@@ -1727,6 +1727,77 @@ typedef LibMCDataResult (*PLibMCDataBuildJobExecution_GetEndTimeStampInMicroseco
*/
typedef LibMCDataResult (*PLibMCDataBuildJobExecution_ComputeElapsedTimeInMicrosecondsPtr) (LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 nGlobalTimerInMicroseconds, bool bThrowExceptionInFailure, LibMCData_uint64 * pElapsedTimeInMicroseconds);
+/**
+* returns the build job execution status as loaded when this instance was created (for example by ListJobExecutions). Does not query the database; use GetStatus for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[out] pExecutionStatus - Status Value
+* @return error code or 0 (success)
+*/
+typedef LibMCDataResult (*PLibMCDataBuildJobExecution_GetLoadedStatusPtr) (LibMCData_BuildJobExecution pBuildJobExecution, LibMCData::eBuildJobExecutionStatus * pExecutionStatus);
+
+/**
+* returns the build job execution status as string, as loaded when this instance was created. Does not query the database; use GetStatusString for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nExecutionStatusBufferSize - size of the buffer (including trailing 0)
+* @param[out] pExecutionStatusNeededChars - will be filled with the count of the written bytes, or needed buffer size.
+* @param[out] pExecutionStatusBuffer - buffer of Status Value, may be NULL
+* @return error code or 0 (success)
+*/
+typedef LibMCDataResult (*PLibMCDataBuildJobExecution_GetLoadedStatusStringPtr) (LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nExecutionStatusBufferSize, LibMCData_uint32* pExecutionStatusNeededChars, char * pExecutionStatusBuffer);
+
+/**
+* returns the build job execution description as loaded when this instance was created. Does not query the database; use GetDescription for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nDescriptionBufferSize - size of the buffer (including trailing 0)
+* @param[out] pDescriptionNeededChars - will be filled with the count of the written bytes, or needed buffer size.
+* @param[out] pDescriptionBuffer - buffer of Description at load time., may be NULL
+* @return error code or 0 (success)
+*/
+typedef LibMCDataResult (*PLibMCDataBuildJobExecution_GetLoadedDescriptionPtr) (LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nDescriptionBufferSize, LibMCData_uint32* pDescriptionNeededChars, char * pDescriptionBuffer);
+
+/**
+* Returns the end time stamp of the build execution as loaded when this instance was created. Loaded status MUST BE Finished or Failed. Does not query the database; use GetEndTimeStampInMicroseconds for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[out] pTimeStampInMicroseconds - TimeStamp when the build ended in Microseconds.
+* @return error code or 0 (success)
+*/
+typedef LibMCDataResult (*PLibMCDataBuildJobExecution_GetLoadedEndTimeStampInMicrosecondsPtr) (LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 * pTimeStampInMicroseconds);
+
+/**
+* Same as ComputeElapsedTimeInMicroseconds, but based on the status and end time stamp loaded when this instance was created. Does not query the database.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nGlobalTimerInMicroseconds - The current session global timer.
+* @param[in] bThrowExceptionInFailure - If true, the method will throw exceptions in any error case. If false, it will just return 0 in any error case.
+* @param[out] pElapsedTimeInMicroseconds - Elapsed time in Microseconds.
+* @return error code or 0 (success)
+*/
+typedef LibMCDataResult (*PLibMCDataBuildJobExecution_ComputeLoadedElapsedTimeInMicrosecondsPtr) (LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 nGlobalTimerInMicroseconds, bool bThrowExceptionInFailure, LibMCData_uint64 * pElapsedTimeInMicroseconds);
+
+/**
+* returns if the parent build job had a thumbnail attached when this instance was loaded. Does not query the database.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[out] pHasThumbnail - Returns true, if the parent build job has an attached thumbnail.
+* @return error code or 0 (success)
+*/
+typedef LibMCDataResult (*PLibMCDataBuildJobExecution_JobHasThumbnailStreamPtr) (LibMCData_BuildJobExecution pBuildJobExecution, bool * pHasThumbnail);
+
+/**
+* returns the UUID of the thumbnail stream of the parent build job, as loaded when this instance was created. Does not query the database.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nUUIDBufferSize - size of the buffer (including trailing 0)
+* @param[out] pUUIDNeededChars - will be filled with the count of the written bytes, or needed buffer size.
+* @param[out] pUUIDBuffer - buffer of Thumbnail UUID. 00000000-0000-0000-0000-000000000000, if no thumbnail is assigned or the parent build job does not exist., may be NULL
+* @return error code or 0 (success)
+*/
+typedef LibMCDataResult (*PLibMCDataBuildJobExecution_GetJobThumbnailStreamUUIDPtr) (LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nUUIDBufferSize, LibMCData_uint32* pUUIDNeededChars, char * pUUIDBuffer);
+
/**
* Adds additional data to the Job Execution.
*
@@ -3890,6 +3961,13 @@ typedef struct {
PLibMCDataBuildJobExecution_GetStartTimeStampInMicrosecondsPtr m_BuildJobExecution_GetStartTimeStampInMicroseconds;
PLibMCDataBuildJobExecution_GetEndTimeStampInMicrosecondsPtr m_BuildJobExecution_GetEndTimeStampInMicroseconds;
PLibMCDataBuildJobExecution_ComputeElapsedTimeInMicrosecondsPtr m_BuildJobExecution_ComputeElapsedTimeInMicroseconds;
+ PLibMCDataBuildJobExecution_GetLoadedStatusPtr m_BuildJobExecution_GetLoadedStatus;
+ PLibMCDataBuildJobExecution_GetLoadedStatusStringPtr m_BuildJobExecution_GetLoadedStatusString;
+ PLibMCDataBuildJobExecution_GetLoadedDescriptionPtr m_BuildJobExecution_GetLoadedDescription;
+ PLibMCDataBuildJobExecution_GetLoadedEndTimeStampInMicrosecondsPtr m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds;
+ PLibMCDataBuildJobExecution_ComputeLoadedElapsedTimeInMicrosecondsPtr m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds;
+ PLibMCDataBuildJobExecution_JobHasThumbnailStreamPtr m_BuildJobExecution_JobHasThumbnailStream;
+ PLibMCDataBuildJobExecution_GetJobThumbnailStreamUUIDPtr m_BuildJobExecution_GetJobThumbnailStreamUUID;
PLibMCDataBuildJobExecution_AddJobExecutionDataPtr m_BuildJobExecution_AddJobExecutionData;
PLibMCDataBuildJobExecution_ListJobExecutionDataByTypePtr m_BuildJobExecution_ListJobExecutionDataByType;
PLibMCDataBuildJobExecution_ListJobExecutionDataPtr m_BuildJobExecution_ListJobExecutionData;
diff --git a/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.hpp b/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.hpp
index 3eb1bab75..f881da46c 100644
--- a/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.hpp
+++ b/Framework/HeadersCore/CppDynamic/libmcdata_dynamic.hpp
@@ -1778,6 +1778,13 @@ class CBuildJobExecution : public CBase {
inline LibMCData_uint64 GetStartTimeStampInMicroseconds();
inline LibMCData_uint64 GetEndTimeStampInMicroseconds();
inline LibMCData_uint64 ComputeElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure);
+ inline eBuildJobExecutionStatus GetLoadedStatus();
+ inline std::string GetLoadedStatusString();
+ inline std::string GetLoadedDescription();
+ inline LibMCData_uint64 GetLoadedEndTimeStampInMicroseconds();
+ inline LibMCData_uint64 ComputeLoadedElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure);
+ inline bool JobHasThumbnailStream();
+ inline std::string GetJobThumbnailStreamUUID();
inline std::string AddJobExecutionData(const std::string & sIdentifier, const std::string & sName, classParam pStream, const eCustomDataType eDataType, const std::string & sUserUUID, const LibMCData_uint64 nAbsoluteTimeStamp);
inline PBuildJobExecutionDataIterator ListJobExecutionDataByType(const eCustomDataType eDataType);
inline PBuildJobExecutionDataIterator ListJobExecutionData();
@@ -2471,6 +2478,13 @@ class CDataModel : public CBase {
pWrapperTable->m_BuildJobExecution_GetStartTimeStampInMicroseconds = nullptr;
pWrapperTable->m_BuildJobExecution_GetEndTimeStampInMicroseconds = nullptr;
pWrapperTable->m_BuildJobExecution_ComputeElapsedTimeInMicroseconds = nullptr;
+ pWrapperTable->m_BuildJobExecution_GetLoadedStatus = nullptr;
+ pWrapperTable->m_BuildJobExecution_GetLoadedStatusString = nullptr;
+ pWrapperTable->m_BuildJobExecution_GetLoadedDescription = nullptr;
+ pWrapperTable->m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds = nullptr;
+ pWrapperTable->m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds = nullptr;
+ pWrapperTable->m_BuildJobExecution_JobHasThumbnailStream = nullptr;
+ pWrapperTable->m_BuildJobExecution_GetJobThumbnailStreamUUID = nullptr;
pWrapperTable->m_BuildJobExecution_AddJobExecutionData = nullptr;
pWrapperTable->m_BuildJobExecution_ListJobExecutionDataByType = nullptr;
pWrapperTable->m_BuildJobExecution_ListJobExecutionData = nullptr;
@@ -4026,6 +4040,69 @@ class CDataModel : public CBase {
if (pWrapperTable->m_BuildJobExecution_ComputeElapsedTimeInMicroseconds == nullptr)
return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+ #ifdef _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedStatus = (PLibMCDataBuildJobExecution_GetLoadedStatusPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_getloadedstatus");
+ #else // _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedStatus = (PLibMCDataBuildJobExecution_GetLoadedStatusPtr) dlsym(hLibrary, "libmcdata_buildjobexecution_getloadedstatus");
+ dlerror();
+ #endif // _WIN32
+ if (pWrapperTable->m_BuildJobExecution_GetLoadedStatus == nullptr)
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ #ifdef _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedStatusString = (PLibMCDataBuildJobExecution_GetLoadedStatusStringPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_getloadedstatusstring");
+ #else // _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedStatusString = (PLibMCDataBuildJobExecution_GetLoadedStatusStringPtr) dlsym(hLibrary, "libmcdata_buildjobexecution_getloadedstatusstring");
+ dlerror();
+ #endif // _WIN32
+ if (pWrapperTable->m_BuildJobExecution_GetLoadedStatusString == nullptr)
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ #ifdef _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedDescription = (PLibMCDataBuildJobExecution_GetLoadedDescriptionPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_getloadeddescription");
+ #else // _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedDescription = (PLibMCDataBuildJobExecution_GetLoadedDescriptionPtr) dlsym(hLibrary, "libmcdata_buildjobexecution_getloadeddescription");
+ dlerror();
+ #endif // _WIN32
+ if (pWrapperTable->m_BuildJobExecution_GetLoadedDescription == nullptr)
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ #ifdef _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds = (PLibMCDataBuildJobExecution_GetLoadedEndTimeStampInMicrosecondsPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_getloadedendtimestampinmicroseconds");
+ #else // _WIN32
+ pWrapperTable->m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds = (PLibMCDataBuildJobExecution_GetLoadedEndTimeStampInMicrosecondsPtr) dlsym(hLibrary, "libmcdata_buildjobexecution_getloadedendtimestampinmicroseconds");
+ dlerror();
+ #endif // _WIN32
+ if (pWrapperTable->m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds == nullptr)
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ #ifdef _WIN32
+ pWrapperTable->m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds = (PLibMCDataBuildJobExecution_ComputeLoadedElapsedTimeInMicrosecondsPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_computeloadedelapsedtimeinmicroseconds");
+ #else // _WIN32
+ pWrapperTable->m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds = (PLibMCDataBuildJobExecution_ComputeLoadedElapsedTimeInMicrosecondsPtr) dlsym(hLibrary, "libmcdata_buildjobexecution_computeloadedelapsedtimeinmicroseconds");
+ dlerror();
+ #endif // _WIN32
+ if (pWrapperTable->m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds == nullptr)
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ #ifdef _WIN32
+ pWrapperTable->m_BuildJobExecution_JobHasThumbnailStream = (PLibMCDataBuildJobExecution_JobHasThumbnailStreamPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_jobhasthumbnailstream");
+ #else // _WIN32
+ pWrapperTable->m_BuildJobExecution_JobHasThumbnailStream = (PLibMCDataBuildJobExecution_JobHasThumbnailStreamPtr) dlsym(hLibrary, "libmcdata_buildjobexecution_jobhasthumbnailstream");
+ dlerror();
+ #endif // _WIN32
+ if (pWrapperTable->m_BuildJobExecution_JobHasThumbnailStream == nullptr)
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ #ifdef _WIN32
+ pWrapperTable->m_BuildJobExecution_GetJobThumbnailStreamUUID = (PLibMCDataBuildJobExecution_GetJobThumbnailStreamUUIDPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_getjobthumbnailstreamuuid");
+ #else // _WIN32
+ pWrapperTable->m_BuildJobExecution_GetJobThumbnailStreamUUID = (PLibMCDataBuildJobExecution_GetJobThumbnailStreamUUIDPtr) dlsym(hLibrary, "libmcdata_buildjobexecution_getjobthumbnailstreamuuid");
+ dlerror();
+ #endif // _WIN32
+ if (pWrapperTable->m_BuildJobExecution_GetJobThumbnailStreamUUID == nullptr)
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
#ifdef _WIN32
pWrapperTable->m_BuildJobExecution_AddJobExecutionData = (PLibMCDataBuildJobExecution_AddJobExecutionDataPtr) GetProcAddress(hLibrary, "libmcdata_buildjobexecution_addjobexecutiondata");
#else // _WIN32
@@ -6268,6 +6345,34 @@ class CDataModel : public CBase {
if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_ComputeElapsedTimeInMicroseconds == nullptr) )
return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+ eLookupError = (*pLookup)("libmcdata_buildjobexecution_getloadedstatus", (void**)&(pWrapperTable->m_BuildJobExecution_GetLoadedStatus));
+ if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_GetLoadedStatus == nullptr) )
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ eLookupError = (*pLookup)("libmcdata_buildjobexecution_getloadedstatusstring", (void**)&(pWrapperTable->m_BuildJobExecution_GetLoadedStatusString));
+ if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_GetLoadedStatusString == nullptr) )
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ eLookupError = (*pLookup)("libmcdata_buildjobexecution_getloadeddescription", (void**)&(pWrapperTable->m_BuildJobExecution_GetLoadedDescription));
+ if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_GetLoadedDescription == nullptr) )
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ eLookupError = (*pLookup)("libmcdata_buildjobexecution_getloadedendtimestampinmicroseconds", (void**)&(pWrapperTable->m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds));
+ if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds == nullptr) )
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ eLookupError = (*pLookup)("libmcdata_buildjobexecution_computeloadedelapsedtimeinmicroseconds", (void**)&(pWrapperTable->m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds));
+ if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds == nullptr) )
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ eLookupError = (*pLookup)("libmcdata_buildjobexecution_jobhasthumbnailstream", (void**)&(pWrapperTable->m_BuildJobExecution_JobHasThumbnailStream));
+ if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_JobHasThumbnailStream == nullptr) )
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
+ eLookupError = (*pLookup)("libmcdata_buildjobexecution_getjobthumbnailstreamuuid", (void**)&(pWrapperTable->m_BuildJobExecution_GetJobThumbnailStreamUUID));
+ if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_GetJobThumbnailStreamUUID == nullptr) )
+ return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
+
eLookupError = (*pLookup)("libmcdata_buildjobexecution_addjobexecutiondata", (void**)&(pWrapperTable->m_BuildJobExecution_AddJobExecutionData));
if ( (eLookupError != 0) || (pWrapperTable->m_BuildJobExecution_AddJobExecutionData == nullptr) )
return LIBMCDATA_ERROR_COULDNOTFINDLIBRARYEXPORT;
@@ -9140,6 +9245,101 @@ class CDataModel : public CBase {
return resultElapsedTimeInMicroseconds;
}
+ /**
+ * CBuildJobExecution::GetLoadedStatus - returns the build job execution status as loaded when this instance was created (for example by ListJobExecutions). Does not query the database; use GetStatus for the current value.
+ * @return Status Value
+ */
+ eBuildJobExecutionStatus CBuildJobExecution::GetLoadedStatus()
+ {
+ eBuildJobExecutionStatus resultExecutionStatus = (eBuildJobExecutionStatus) 0;
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetLoadedStatus(m_pHandle, &resultExecutionStatus));
+
+ return resultExecutionStatus;
+ }
+
+ /**
+ * CBuildJobExecution::GetLoadedStatusString - returns the build job execution status as string, as loaded when this instance was created. Does not query the database; use GetStatusString for the current value.
+ * @return Status Value
+ */
+ std::string CBuildJobExecution::GetLoadedStatusString()
+ {
+ LibMCData_uint32 bytesNeededExecutionStatus = 0;
+ LibMCData_uint32 bytesWrittenExecutionStatus = 0;
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetLoadedStatusString(m_pHandle, 0, &bytesNeededExecutionStatus, nullptr));
+ std::vector bufferExecutionStatus(bytesNeededExecutionStatus);
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetLoadedStatusString(m_pHandle, bytesNeededExecutionStatus, &bytesWrittenExecutionStatus, &bufferExecutionStatus[0]));
+
+ return std::string(&bufferExecutionStatus[0]);
+ }
+
+ /**
+ * CBuildJobExecution::GetLoadedDescription - returns the build job execution description as loaded when this instance was created. Does not query the database; use GetDescription for the current value.
+ * @return Description at load time.
+ */
+ std::string CBuildJobExecution::GetLoadedDescription()
+ {
+ LibMCData_uint32 bytesNeededDescription = 0;
+ LibMCData_uint32 bytesWrittenDescription = 0;
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetLoadedDescription(m_pHandle, 0, &bytesNeededDescription, nullptr));
+ std::vector bufferDescription(bytesNeededDescription);
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetLoadedDescription(m_pHandle, bytesNeededDescription, &bytesWrittenDescription, &bufferDescription[0]));
+
+ return std::string(&bufferDescription[0]);
+ }
+
+ /**
+ * CBuildJobExecution::GetLoadedEndTimeStampInMicroseconds - Returns the end time stamp of the build execution as loaded when this instance was created. Loaded status MUST BE Finished or Failed. Does not query the database; use GetEndTimeStampInMicroseconds for the current value.
+ * @return TimeStamp when the build ended in Microseconds.
+ */
+ LibMCData_uint64 CBuildJobExecution::GetLoadedEndTimeStampInMicroseconds()
+ {
+ LibMCData_uint64 resultTimeStampInMicroseconds = 0;
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetLoadedEndTimeStampInMicroseconds(m_pHandle, &resultTimeStampInMicroseconds));
+
+ return resultTimeStampInMicroseconds;
+ }
+
+ /**
+ * CBuildJobExecution::ComputeLoadedElapsedTimeInMicroseconds - Same as ComputeElapsedTimeInMicroseconds, but based on the status and end time stamp loaded when this instance was created. Does not query the database.
+ * @param[in] nGlobalTimerInMicroseconds - The current session global timer.
+ * @param[in] bThrowExceptionInFailure - If true, the method will throw exceptions in any error case. If false, it will just return 0 in any error case.
+ * @return Elapsed time in Microseconds.
+ */
+ LibMCData_uint64 CBuildJobExecution::ComputeLoadedElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure)
+ {
+ LibMCData_uint64 resultElapsedTimeInMicroseconds = 0;
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_ComputeLoadedElapsedTimeInMicroseconds(m_pHandle, nGlobalTimerInMicroseconds, bThrowExceptionInFailure, &resultElapsedTimeInMicroseconds));
+
+ return resultElapsedTimeInMicroseconds;
+ }
+
+ /**
+ * CBuildJobExecution::JobHasThumbnailStream - returns if the parent build job had a thumbnail attached when this instance was loaded. Does not query the database.
+ * @return Returns true, if the parent build job has an attached thumbnail.
+ */
+ bool CBuildJobExecution::JobHasThumbnailStream()
+ {
+ bool resultHasThumbnail = 0;
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_JobHasThumbnailStream(m_pHandle, &resultHasThumbnail));
+
+ return resultHasThumbnail;
+ }
+
+ /**
+ * CBuildJobExecution::GetJobThumbnailStreamUUID - returns the UUID of the thumbnail stream of the parent build job, as loaded when this instance was created. Does not query the database.
+ * @return Thumbnail UUID. 00000000-0000-0000-0000-000000000000, if no thumbnail is assigned or the parent build job does not exist.
+ */
+ std::string CBuildJobExecution::GetJobThumbnailStreamUUID()
+ {
+ LibMCData_uint32 bytesNeededUUID = 0;
+ LibMCData_uint32 bytesWrittenUUID = 0;
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetJobThumbnailStreamUUID(m_pHandle, 0, &bytesNeededUUID, nullptr));
+ std::vector bufferUUID(bytesNeededUUID);
+ CheckError(m_pWrapper->m_WrapperTable.m_BuildJobExecution_GetJobThumbnailStreamUUID(m_pHandle, bytesNeededUUID, &bytesWrittenUUID, &bufferUUID[0]));
+
+ return std::string(&bufferUUID[0]);
+ }
+
/**
* CBuildJobExecution::AddJobExecutionData - Adds additional data to the Job Execution.
* @param[in] sIdentifier - Unique identifier for the job data.
diff --git a/Framework/InterfacesCore/libmcdata_abi.hpp b/Framework/InterfacesCore/libmcdata_abi.hpp
index 98e1f41ee..352692680 100644
--- a/Framework/InterfacesCore/libmcdata_abi.hpp
+++ b/Framework/InterfacesCore/libmcdata_abi.hpp
@@ -1740,6 +1740,77 @@ LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_getendtimestampin
*/
LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_computeelapsedtimeinmicroseconds(LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 nGlobalTimerInMicroseconds, bool bThrowExceptionInFailure, LibMCData_uint64 * pElapsedTimeInMicroseconds);
+/**
+* returns the build job execution status as loaded when this instance was created (for example by ListJobExecutions). Does not query the database; use GetStatus for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[out] pExecutionStatus - Status Value
+* @return error code or 0 (success)
+*/
+LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_getloadedstatus(LibMCData_BuildJobExecution pBuildJobExecution, LibMCData::eBuildJobExecutionStatus * pExecutionStatus);
+
+/**
+* returns the build job execution status as string, as loaded when this instance was created. Does not query the database; use GetStatusString for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nExecutionStatusBufferSize - size of the buffer (including trailing 0)
+* @param[out] pExecutionStatusNeededChars - will be filled with the count of the written bytes, or needed buffer size.
+* @param[out] pExecutionStatusBuffer - buffer of Status Value, may be NULL
+* @return error code or 0 (success)
+*/
+LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_getloadedstatusstring(LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nExecutionStatusBufferSize, LibMCData_uint32* pExecutionStatusNeededChars, char * pExecutionStatusBuffer);
+
+/**
+* returns the build job execution description as loaded when this instance was created. Does not query the database; use GetDescription for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nDescriptionBufferSize - size of the buffer (including trailing 0)
+* @param[out] pDescriptionNeededChars - will be filled with the count of the written bytes, or needed buffer size.
+* @param[out] pDescriptionBuffer - buffer of Description at load time., may be NULL
+* @return error code or 0 (success)
+*/
+LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_getloadeddescription(LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nDescriptionBufferSize, LibMCData_uint32* pDescriptionNeededChars, char * pDescriptionBuffer);
+
+/**
+* Returns the end time stamp of the build execution as loaded when this instance was created. Loaded status MUST BE Finished or Failed. Does not query the database; use GetEndTimeStampInMicroseconds for the current value.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[out] pTimeStampInMicroseconds - TimeStamp when the build ended in Microseconds.
+* @return error code or 0 (success)
+*/
+LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_getloadedendtimestampinmicroseconds(LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 * pTimeStampInMicroseconds);
+
+/**
+* Same as ComputeElapsedTimeInMicroseconds, but based on the status and end time stamp loaded when this instance was created. Does not query the database.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nGlobalTimerInMicroseconds - The current session global timer.
+* @param[in] bThrowExceptionInFailure - If true, the method will throw exceptions in any error case. If false, it will just return 0 in any error case.
+* @param[out] pElapsedTimeInMicroseconds - Elapsed time in Microseconds.
+* @return error code or 0 (success)
+*/
+LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_computeloadedelapsedtimeinmicroseconds(LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 nGlobalTimerInMicroseconds, bool bThrowExceptionInFailure, LibMCData_uint64 * pElapsedTimeInMicroseconds);
+
+/**
+* returns if the parent build job had a thumbnail attached when this instance was loaded. Does not query the database.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[out] pHasThumbnail - Returns true, if the parent build job has an attached thumbnail.
+* @return error code or 0 (success)
+*/
+LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_jobhasthumbnailstream(LibMCData_BuildJobExecution pBuildJobExecution, bool * pHasThumbnail);
+
+/**
+* returns the UUID of the thumbnail stream of the parent build job, as loaded when this instance was created. Does not query the database.
+*
+* @param[in] pBuildJobExecution - BuildJobExecution instance.
+* @param[in] nUUIDBufferSize - size of the buffer (including trailing 0)
+* @param[out] pUUIDNeededChars - will be filled with the count of the written bytes, or needed buffer size.
+* @param[out] pUUIDBuffer - buffer of Thumbnail UUID. 00000000-0000-0000-0000-000000000000, if no thumbnail is assigned or the parent build job does not exist., may be NULL
+* @return error code or 0 (success)
+*/
+LIBMCDATA_DECLSPEC LibMCDataResult libmcdata_buildjobexecution_getjobthumbnailstreamuuid(LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nUUIDBufferSize, LibMCData_uint32* pUUIDNeededChars, char * pUUIDBuffer);
+
/**
* Adds additional data to the Job Execution.
*
diff --git a/Framework/InterfacesCore/libmcdata_interfaces.hpp b/Framework/InterfacesCore/libmcdata_interfaces.hpp
index 5d52dca6e..f3a24c3ed 100644
--- a/Framework/InterfacesCore/libmcdata_interfaces.hpp
+++ b/Framework/InterfacesCore/libmcdata_interfaces.hpp
@@ -1607,6 +1607,50 @@ class IBuildJobExecution : public virtual IBase {
*/
virtual LibMCData_uint64 ComputeElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure) = 0;
+ /**
+ * IBuildJobExecution::GetLoadedStatus - returns the build job execution status as loaded when this instance was created (for example by ListJobExecutions). Does not query the database; use GetStatus for the current value.
+ * @return Status Value
+ */
+ virtual LibMCData::eBuildJobExecutionStatus GetLoadedStatus() = 0;
+
+ /**
+ * IBuildJobExecution::GetLoadedStatusString - returns the build job execution status as string, as loaded when this instance was created. Does not query the database; use GetStatusString for the current value.
+ * @return Status Value
+ */
+ virtual std::string GetLoadedStatusString() = 0;
+
+ /**
+ * IBuildJobExecution::GetLoadedDescription - returns the build job execution description as loaded when this instance was created. Does not query the database; use GetDescription for the current value.
+ * @return Description at load time.
+ */
+ virtual std::string GetLoadedDescription() = 0;
+
+ /**
+ * IBuildJobExecution::GetLoadedEndTimeStampInMicroseconds - Returns the end time stamp of the build execution as loaded when this instance was created. Loaded status MUST BE Finished or Failed. Does not query the database; use GetEndTimeStampInMicroseconds for the current value.
+ * @return TimeStamp when the build ended in Microseconds.
+ */
+ virtual LibMCData_uint64 GetLoadedEndTimeStampInMicroseconds() = 0;
+
+ /**
+ * IBuildJobExecution::ComputeLoadedElapsedTimeInMicroseconds - Same as ComputeElapsedTimeInMicroseconds, but based on the status and end time stamp loaded when this instance was created. Does not query the database.
+ * @param[in] nGlobalTimerInMicroseconds - The current session global timer.
+ * @param[in] bThrowExceptionInFailure - If true, the method will throw exceptions in any error case. If false, it will just return 0 in any error case.
+ * @return Elapsed time in Microseconds.
+ */
+ virtual LibMCData_uint64 ComputeLoadedElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure) = 0;
+
+ /**
+ * IBuildJobExecution::JobHasThumbnailStream - returns if the parent build job had a thumbnail attached when this instance was loaded. Does not query the database.
+ * @return Returns true, if the parent build job has an attached thumbnail.
+ */
+ virtual bool JobHasThumbnailStream() = 0;
+
+ /**
+ * IBuildJobExecution::GetJobThumbnailStreamUUID - returns the UUID of the thumbnail stream of the parent build job, as loaded when this instance was created. Does not query the database.
+ * @return Thumbnail UUID. 00000000-0000-0000-0000-000000000000, if no thumbnail is assigned or the parent build job does not exist.
+ */
+ virtual std::string GetJobThumbnailStreamUUID() = 0;
+
/**
* IBuildJobExecution::AddJobExecutionData - Adds additional data to the Job Execution.
* @param[in] sIdentifier - Unique identifier for the job data.
diff --git a/Framework/InterfacesCore/libmcdata_interfacewrapper.cpp b/Framework/InterfacesCore/libmcdata_interfacewrapper.cpp
index 92ad92f6b..cd8cd1ea9 100644
--- a/Framework/InterfacesCore/libmcdata_interfacewrapper.cpp
+++ b/Framework/InterfacesCore/libmcdata_interfacewrapper.cpp
@@ -5359,6 +5359,254 @@ LibMCDataResult libmcdata_buildjobexecution_computeelapsedtimeinmicroseconds(Lib
}
}
+LibMCDataResult libmcdata_buildjobexecution_getloadedstatus(LibMCData_BuildJobExecution pBuildJobExecution, eLibMCDataBuildJobExecutionStatus * pExecutionStatus)
+{
+ IBase* pIBaseClass = (IBase *)pBuildJobExecution;
+
+ try {
+ if (pExecutionStatus == nullptr)
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_INVALIDPARAM);
+ IBuildJobExecution* pIBuildJobExecution = dynamic_cast(pIBaseClass);
+ if (!pIBuildJobExecution)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+
+ *pExecutionStatus = pIBuildJobExecution->GetLoadedStatus();
+
+ return LIBMCDATA_SUCCESS;
+ }
+ catch (ELibMCDataInterfaceException & Exception) {
+ return handleLibMCDataException(pIBaseClass, Exception);
+ }
+ catch (std::exception & StdException) {
+ return handleStdException(pIBaseClass, StdException);
+ }
+ catch (...) {
+ return handleUnhandledException(pIBaseClass);
+ }
+}
+
+LibMCDataResult libmcdata_buildjobexecution_getloadedstatusstring(LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nExecutionStatusBufferSize, LibMCData_uint32* pExecutionStatusNeededChars, char * pExecutionStatusBuffer)
+{
+ IBase* pIBaseClass = (IBase *)pBuildJobExecution;
+
+ try {
+ if ( (!pExecutionStatusBuffer) && !(pExecutionStatusNeededChars) )
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_INVALIDPARAM);
+ std::string sExecutionStatus("");
+ IBuildJobExecution* pIBuildJobExecution = dynamic_cast(pIBaseClass);
+ if (!pIBuildJobExecution)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+
+ bool isCacheCall = (pExecutionStatusBuffer == nullptr);
+ if (isCacheCall) {
+ sExecutionStatus = pIBuildJobExecution->GetLoadedStatusString();
+
+ pIBuildJobExecution->_setCache (new ParameterCache_1 (sExecutionStatus));
+ }
+ else {
+ auto cache = dynamic_cast*> (pIBuildJobExecution->_getCache ());
+ if (cache == nullptr)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+ cache->retrieveData (sExecutionStatus);
+ pIBuildJobExecution->_setCache (nullptr);
+ }
+
+ if (pExecutionStatusNeededChars)
+ *pExecutionStatusNeededChars = (LibMCData_uint32) (sExecutionStatus.size()+1);
+ if (pExecutionStatusBuffer) {
+ if (sExecutionStatus.size() >= nExecutionStatusBufferSize)
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_BUFFERTOOSMALL);
+ for (size_t iExecutionStatus = 0; iExecutionStatus < sExecutionStatus.size(); iExecutionStatus++)
+ pExecutionStatusBuffer[iExecutionStatus] = sExecutionStatus[iExecutionStatus];
+ pExecutionStatusBuffer[sExecutionStatus.size()] = 0;
+ }
+ return LIBMCDATA_SUCCESS;
+ }
+ catch (ELibMCDataInterfaceException & Exception) {
+ return handleLibMCDataException(pIBaseClass, Exception);
+ }
+ catch (std::exception & StdException) {
+ return handleStdException(pIBaseClass, StdException);
+ }
+ catch (...) {
+ return handleUnhandledException(pIBaseClass);
+ }
+}
+
+LibMCDataResult libmcdata_buildjobexecution_getloadeddescription(LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nDescriptionBufferSize, LibMCData_uint32* pDescriptionNeededChars, char * pDescriptionBuffer)
+{
+ IBase* pIBaseClass = (IBase *)pBuildJobExecution;
+
+ try {
+ if ( (!pDescriptionBuffer) && !(pDescriptionNeededChars) )
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_INVALIDPARAM);
+ std::string sDescription("");
+ IBuildJobExecution* pIBuildJobExecution = dynamic_cast(pIBaseClass);
+ if (!pIBuildJobExecution)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+
+ bool isCacheCall = (pDescriptionBuffer == nullptr);
+ if (isCacheCall) {
+ sDescription = pIBuildJobExecution->GetLoadedDescription();
+
+ pIBuildJobExecution->_setCache (new ParameterCache_1 (sDescription));
+ }
+ else {
+ auto cache = dynamic_cast*> (pIBuildJobExecution->_getCache ());
+ if (cache == nullptr)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+ cache->retrieveData (sDescription);
+ pIBuildJobExecution->_setCache (nullptr);
+ }
+
+ if (pDescriptionNeededChars)
+ *pDescriptionNeededChars = (LibMCData_uint32) (sDescription.size()+1);
+ if (pDescriptionBuffer) {
+ if (sDescription.size() >= nDescriptionBufferSize)
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_BUFFERTOOSMALL);
+ for (size_t iDescription = 0; iDescription < sDescription.size(); iDescription++)
+ pDescriptionBuffer[iDescription] = sDescription[iDescription];
+ pDescriptionBuffer[sDescription.size()] = 0;
+ }
+ return LIBMCDATA_SUCCESS;
+ }
+ catch (ELibMCDataInterfaceException & Exception) {
+ return handleLibMCDataException(pIBaseClass, Exception);
+ }
+ catch (std::exception & StdException) {
+ return handleStdException(pIBaseClass, StdException);
+ }
+ catch (...) {
+ return handleUnhandledException(pIBaseClass);
+ }
+}
+
+LibMCDataResult libmcdata_buildjobexecution_getloadedendtimestampinmicroseconds(LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 * pTimeStampInMicroseconds)
+{
+ IBase* pIBaseClass = (IBase *)pBuildJobExecution;
+
+ try {
+ if (pTimeStampInMicroseconds == nullptr)
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_INVALIDPARAM);
+ IBuildJobExecution* pIBuildJobExecution = dynamic_cast(pIBaseClass);
+ if (!pIBuildJobExecution)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+
+ *pTimeStampInMicroseconds = pIBuildJobExecution->GetLoadedEndTimeStampInMicroseconds();
+
+ return LIBMCDATA_SUCCESS;
+ }
+ catch (ELibMCDataInterfaceException & Exception) {
+ return handleLibMCDataException(pIBaseClass, Exception);
+ }
+ catch (std::exception & StdException) {
+ return handleStdException(pIBaseClass, StdException);
+ }
+ catch (...) {
+ return handleUnhandledException(pIBaseClass);
+ }
+}
+
+LibMCDataResult libmcdata_buildjobexecution_computeloadedelapsedtimeinmicroseconds(LibMCData_BuildJobExecution pBuildJobExecution, LibMCData_uint64 nGlobalTimerInMicroseconds, bool bThrowExceptionInFailure, LibMCData_uint64 * pElapsedTimeInMicroseconds)
+{
+ IBase* pIBaseClass = (IBase *)pBuildJobExecution;
+
+ try {
+ if (pElapsedTimeInMicroseconds == nullptr)
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_INVALIDPARAM);
+ IBuildJobExecution* pIBuildJobExecution = dynamic_cast(pIBaseClass);
+ if (!pIBuildJobExecution)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+
+ *pElapsedTimeInMicroseconds = pIBuildJobExecution->ComputeLoadedElapsedTimeInMicroseconds(nGlobalTimerInMicroseconds, bThrowExceptionInFailure);
+
+ return LIBMCDATA_SUCCESS;
+ }
+ catch (ELibMCDataInterfaceException & Exception) {
+ return handleLibMCDataException(pIBaseClass, Exception);
+ }
+ catch (std::exception & StdException) {
+ return handleStdException(pIBaseClass, StdException);
+ }
+ catch (...) {
+ return handleUnhandledException(pIBaseClass);
+ }
+}
+
+LibMCDataResult libmcdata_buildjobexecution_jobhasthumbnailstream(LibMCData_BuildJobExecution pBuildJobExecution, bool * pHasThumbnail)
+{
+ IBase* pIBaseClass = (IBase *)pBuildJobExecution;
+
+ try {
+ if (pHasThumbnail == nullptr)
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_INVALIDPARAM);
+ IBuildJobExecution* pIBuildJobExecution = dynamic_cast(pIBaseClass);
+ if (!pIBuildJobExecution)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+
+ *pHasThumbnail = pIBuildJobExecution->JobHasThumbnailStream();
+
+ return LIBMCDATA_SUCCESS;
+ }
+ catch (ELibMCDataInterfaceException & Exception) {
+ return handleLibMCDataException(pIBaseClass, Exception);
+ }
+ catch (std::exception & StdException) {
+ return handleStdException(pIBaseClass, StdException);
+ }
+ catch (...) {
+ return handleUnhandledException(pIBaseClass);
+ }
+}
+
+LibMCDataResult libmcdata_buildjobexecution_getjobthumbnailstreamuuid(LibMCData_BuildJobExecution pBuildJobExecution, const LibMCData_uint32 nUUIDBufferSize, LibMCData_uint32* pUUIDNeededChars, char * pUUIDBuffer)
+{
+ IBase* pIBaseClass = (IBase *)pBuildJobExecution;
+
+ try {
+ if ( (!pUUIDBuffer) && !(pUUIDNeededChars) )
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_INVALIDPARAM);
+ std::string sUUID("");
+ IBuildJobExecution* pIBuildJobExecution = dynamic_cast(pIBaseClass);
+ if (!pIBuildJobExecution)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+
+ bool isCacheCall = (pUUIDBuffer == nullptr);
+ if (isCacheCall) {
+ sUUID = pIBuildJobExecution->GetJobThumbnailStreamUUID();
+
+ pIBuildJobExecution->_setCache (new ParameterCache_1 (sUUID));
+ }
+ else {
+ auto cache = dynamic_cast*> (pIBuildJobExecution->_getCache ());
+ if (cache == nullptr)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDCAST);
+ cache->retrieveData (sUUID);
+ pIBuildJobExecution->_setCache (nullptr);
+ }
+
+ if (pUUIDNeededChars)
+ *pUUIDNeededChars = (LibMCData_uint32) (sUUID.size()+1);
+ if (pUUIDBuffer) {
+ if (sUUID.size() >= nUUIDBufferSize)
+ throw ELibMCDataInterfaceException (LIBMCDATA_ERROR_BUFFERTOOSMALL);
+ for (size_t iUUID = 0; iUUID < sUUID.size(); iUUID++)
+ pUUIDBuffer[iUUID] = sUUID[iUUID];
+ pUUIDBuffer[sUUID.size()] = 0;
+ }
+ return LIBMCDATA_SUCCESS;
+ }
+ catch (ELibMCDataInterfaceException & Exception) {
+ return handleLibMCDataException(pIBaseClass, Exception);
+ }
+ catch (std::exception & StdException) {
+ return handleStdException(pIBaseClass, StdException);
+ }
+ catch (...) {
+ return handleUnhandledException(pIBaseClass);
+ }
+}
+
LibMCDataResult libmcdata_buildjobexecution_addjobexecutiondata(LibMCData_BuildJobExecution pBuildJobExecution, const char * pIdentifier, const char * pName, LibMCData_StorageStream pStream, eLibMCDataCustomDataType eDataType, const char * pUserUUID, LibMCData_uint64 nAbsoluteTimeStamp, const LibMCData_uint32 nDataUUIDBufferSize, LibMCData_uint32* pDataUUIDNeededChars, char * pDataUUIDBuffer)
{
IBase* pIBaseClass = (IBase *)pBuildJobExecution;
@@ -12059,6 +12307,20 @@ LibMCDataResult LibMCData::Impl::LibMCData_GetProcAddress (const char * pProcNam
*ppProcAddress = (void*) &libmcdata_buildjobexecution_getendtimestampinmicroseconds;
if (sProcName == "libmcdata_buildjobexecution_computeelapsedtimeinmicroseconds")
*ppProcAddress = (void*) &libmcdata_buildjobexecution_computeelapsedtimeinmicroseconds;
+ if (sProcName == "libmcdata_buildjobexecution_getloadedstatus")
+ *ppProcAddress = (void*) &libmcdata_buildjobexecution_getloadedstatus;
+ if (sProcName == "libmcdata_buildjobexecution_getloadedstatusstring")
+ *ppProcAddress = (void*) &libmcdata_buildjobexecution_getloadedstatusstring;
+ if (sProcName == "libmcdata_buildjobexecution_getloadeddescription")
+ *ppProcAddress = (void*) &libmcdata_buildjobexecution_getloadeddescription;
+ if (sProcName == "libmcdata_buildjobexecution_getloadedendtimestampinmicroseconds")
+ *ppProcAddress = (void*) &libmcdata_buildjobexecution_getloadedendtimestampinmicroseconds;
+ if (sProcName == "libmcdata_buildjobexecution_computeloadedelapsedtimeinmicroseconds")
+ *ppProcAddress = (void*) &libmcdata_buildjobexecution_computeloadedelapsedtimeinmicroseconds;
+ if (sProcName == "libmcdata_buildjobexecution_jobhasthumbnailstream")
+ *ppProcAddress = (void*) &libmcdata_buildjobexecution_jobhasthumbnailstream;
+ if (sProcName == "libmcdata_buildjobexecution_getjobthumbnailstreamuuid")
+ *ppProcAddress = (void*) &libmcdata_buildjobexecution_getjobthumbnailstreamuuid;
if (sProcName == "libmcdata_buildjobexecution_addjobexecutiondata")
*ppProcAddress = (void*) &libmcdata_buildjobexecution_addjobexecutiondata;
if (sProcName == "libmcdata_buildjobexecution_listjobexecutiondatabytype")
diff --git a/Implementation/API/amc_api_executionlistentry.cpp b/Implementation/API/amc_api_executionlistentry.cpp
new file mode 100644
index 000000000..a71b030e8
--- /dev/null
+++ b/Implementation/API/amc_api_executionlistentry.cpp
@@ -0,0 +1,71 @@
+/*++
+
+Copyright (C) 2020 Autodesk Inc.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the Autodesk Inc. nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL AUTODESK INC. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+#include "amc_api_executionlistentry.hpp"
+#include "libmc_interfaceexception.hpp"
+#include "common_chrono.hpp"
+
+#include
+
+namespace AMC {
+
+ void writeExecutionListEntryToJSON(CJSONWriterObject& entryObject, LibMCData::PBuildJobExecution pExecution, uint64_t nGlobalTimerInMicroseconds)
+ {
+ if (pExecution.get() == nullptr)
+ throw ELibMCInterfaceException(LIBMC_ERROR_INVALIDPARAM);
+
+ LibMCData::eBuildJobExecutionStatus status = pExecution->GetLoadedStatus();
+
+ uint64_t nStartTimeStamp = pExecution->GetStartTimeStampInMicroseconds();
+ uint64_t nEndTimeStamp = 0;
+ // Failed (e.g. aborted) executions also have an end time stamp.
+ if ((status == LibMCData::eBuildJobExecutionStatus::Finished) || (status == LibMCData::eBuildJobExecutionStatus::Failed))
+ nEndTimeStamp = pExecution->GetLoadedEndTimeStampInMicroseconds();
+
+ int64_t nDurationInSeconds = pExecution->ComputeLoadedElapsedTimeInMicroseconds(nGlobalTimerInMicroseconds, false) / 1000000LL;
+
+ entryObject.addString("executionuuid", pExecution->GetExecutionUUID());
+ entryObject.addString("executionname", pExecution->GetJobName());
+ entryObject.addString("executiondescription", pExecution->GetLoadedDescription());
+ entryObject.addString("executionstarttimestamp", AMCCommon::CChrono::convertToISO8601TimeUTC(nStartTimeStamp));
+ entryObject.addString("executionendtimestamp", AMCCommon::CChrono::convertToISO8601TimeUTC(nEndTimeStamp));
+ entryObject.addInteger("executionduration", nDurationInSeconds);
+ entryObject.addString("executionstatus", pExecution->GetLoadedStatusString());
+ entryObject.addString("executionbuildstatus", pExecution->GetJobStatusString());
+ entryObject.addInteger("executionlayercount", pExecution->GetJobLayerCount());
+
+ entryObject.addString("jobuuid", pExecution->GetJobUUID());
+
+ // A missing job row is loaded as a Deleted job without name and thumbnail.
+ if (pExecution->JobHasThumbnailStream())
+ entryObject.addString("executionthumbnail", pExecution->GetJobThumbnailStreamUUID());
+ }
+
+}
diff --git a/Implementation/API/amc_api_executionlistentry.hpp b/Implementation/API/amc_api_executionlistentry.hpp
new file mode 100644
index 000000000..c09be7e11
--- /dev/null
+++ b/Implementation/API/amc_api_executionlistentry.hpp
@@ -0,0 +1,55 @@
+/*++
+
+Copyright (C) 2020 Autodesk Inc.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the Autodesk Inc. nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL AUTODESK INC. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+#ifndef __AMC_API_EXECUTIONLISTENTRY
+#define __AMC_API_EXECUTIONLISTENTRY
+
+#include
+
+#include "amc_jsonwriter.hpp"
+#include "libmcdata_dynamic.hpp"
+
+namespace AMC {
+
+ // Writes one entry of the /api/executions list (full list and ?since= deltas alike).
+ // Uses only the values that the list query loaded with the execution (GetLoaded*, the parent
+ // job fields), so it runs no database query:
+ // - a job whose row is missing is written with an empty name and without thumbnail,
+ // - executionthumbnail is omitted when the job has no thumbnail; the client then shows its
+ // default thumbnail. A deleted job keeps its thumbnail,
+ // - executionendtimestamp is the end of a Finished or Failed execution, and the time stamp 0
+ // (1970-01-01) otherwise,
+ // - executionduration is in whole seconds; an execution in process of the current journal is
+ // measured against nGlobalTimerInMicroseconds, one of an earlier journal reports 0.
+ void writeExecutionListEntryToJSON(CJSONWriterObject& entryObject, LibMCData::PBuildJobExecution pExecution, uint64_t nGlobalTimerInMicroseconds);
+
+}
+
+#endif //__AMC_API_EXECUTIONLISTENTRY
diff --git a/Implementation/API/amc_api_handler_executions.cpp b/Implementation/API/amc_api_handler_executions.cpp
index 35d4926f9..859d937cb 100644
--- a/Implementation/API/amc_api_handler_executions.cpp
+++ b/Implementation/API/amc_api_handler_executions.cpp
@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#define __AMCIMPL_API_CONSTANTS
#include "amc_api_handler_executions.hpp"
+#include "amc_api_executionlistentry.hpp"
#include "amc_api_constants.hpp"
#include "libmc_interfaceexception.hpp"
#include "libmcdata_dynamic.hpp"
@@ -79,44 +80,12 @@ void CAPIHandler_Executions::handleListExecutionsRequest(CJSONWriter& writer, CA
CJSONWriterArray executionArray(writer);
+ // Every entry is written from the values the list query loaded, so the list costs one query
+ // regardless of its length. One timer value for all entries keeps their durations consistent.
+ uint64_t nGlobalTimer = pGlobalChrono->getUTCTimeStampInMicrosecondsSince1970();
while (pExecutionIterator->MoveNext()) {
- auto pExecution = pExecutionIterator->GetCurrentJobExecution();
-
- LibMCData::eBuildJobExecutionStatus status = pExecution->GetStatus();
- std::string sStatusString = pExecution->GetStatusString();
-
- uint64_t nStartTimeStamp = pExecution->GetStartTimeStampInMicroseconds();
- uint64_t nEndTimeStamp = 0;
- // Failed (e.g. aborted) executions also have an end time stamp.
- if ((status == LibMCData::eBuildJobExecutionStatus::Finished) || (status == LibMCData::eBuildJobExecutionStatus::Failed)) {
- nEndTimeStamp = pExecution->GetEndTimeStampInMicroseconds();
- }
-
- int64_t nDurationInSeconds = pExecution->ComputeElapsedTimeInMicroseconds(pGlobalChrono->getUTCTimeStampInMicrosecondsSince1970(), false) / 1000000LL;
-
CJSONWriterObject entryObject(writer);
- entryObject.addString("executionuuid", pExecution->GetExecutionUUID());
- entryObject.addString("executionname", pExecution->GetJobName());
- entryObject.addString("executiondescription", pExecution->GetDescription());
- entryObject.addString("executionstarttimestamp", AMCCommon::CChrono::convertToISO8601TimeUTC(nStartTimeStamp));
- entryObject.addString("executionendtimestamp", AMCCommon::CChrono::convertToISO8601TimeUTC(nEndTimeStamp));
- entryObject.addInteger("executionduration", nDurationInSeconds);
- entryObject.addString("executionstatus", sStatusString);
- entryObject.addString("executionbuildstatus", pExecution->GetJobStatusString());
- entryObject.addInteger("executionlayercount", pExecution->GetJobLayerCount());
-
- std::string sJobUUID = pExecution->GetJobUUID();
- entryObject.addString("jobuuid", sJobUUID);
-
- try {
- auto pBuildJob = pBuildJobHandler->RetrieveJob(sJobUUID);
- if (pBuildJob->HasThumbnailStream())
- entryObject.addString("executionthumbnail", pBuildJob->GetThumbnailStreamUUID());
- }
- catch (...) {
- // Job may have been deleted; skip thumbnail
- }
-
+ writeExecutionListEntryToJSON(entryObject, pExecutionIterator->GetCurrentJobExecution(), nGlobalTimer);
executionArray.addObject(entryObject);
}
diff --git a/Implementation/LibMCData/libmcdata_buildjob.cpp b/Implementation/LibMCData/libmcdata_buildjob.cpp
index 949d868a2..a371eaaf9 100644
--- a/Implementation/LibMCData/libmcdata_buildjob.cpp
+++ b/Implementation/LibMCData/libmcdata_buildjob.cpp
@@ -213,7 +213,12 @@ std::string CBuildJob::GetCreatorName()
bool CBuildJob::HasThumbnailStream()
{
- return (m_sThumbnailStreamUUID != AMCCommon::CUtils::createEmptyUUID());
+ return thumbnailStreamUUIDIsSet(m_sThumbnailStreamUUID);
+}
+
+bool CBuildJob::thumbnailStreamUUIDIsSet(const std::string& sThumbnailStreamUUID)
+{
+ return (sThumbnailStreamUUID != AMCCommon::CUtils::createEmptyUUID());
}
std::string CBuildJob::GetThumbnailStreamUUID()
@@ -705,7 +710,18 @@ IBuildJobExecution* CBuildJob::CreateBuildJobExecution(const std::string& sDescr
pTransaction->commit();
- return new CBuildJobExecution(m_pSQLHandler, sExecutionUUID, m_sUUID, sJournalUUID, sUserUUID, nAbsoluteStartTimeStampInMicrosecondsSince1970, m_sName, m_eJobStatus, m_nLayerCount, m_pStorageState);
+ sBuildJobExecutionLoadedJob loadedJob;
+ loadedJob.m_sName = m_sName;
+ loadedJob.m_Status = m_eJobStatus;
+ loadedJob.m_nLayerCount = m_nLayerCount;
+ loadedJob.m_sThumbnailStreamUUID = m_sThumbnailStreamUUID;
+
+ sBuildJobExecutionLoadedValues loadedValues;
+ loadedValues.m_Status = LibMCData::eBuildJobExecutionStatus::InProcess;
+ loadedValues.m_sDescription = sDescription;
+ // No end time stamp while in process
+
+ return new CBuildJobExecution(m_pSQLHandler, sExecutionUUID, m_sUUID, sJournalUUID, sUserUUID, nAbsoluteStartTimeStampInMicrosecondsSince1970, loadedJob, loadedValues, m_pStorageState);
}
@@ -722,7 +738,7 @@ IBuildJobExecutionIterator* CBuildJob::RetrieveBuildJobExecutions(const std::str
if (!sJournalUUIDFilter.empty())
sJournalQuery = " AND buildjobexecutions.journaluuid=?";
- std::string sSelectQuery = "SELECT buildjobexecutions.uuid, buildjobexecutions.jobuuid, buildjobexecutions.journaluuid, buildjobexecutions.useruuid, buildjobexecutions.startjournaltimestamp, buildjobs.name, buildjobs.status, buildjobs.layercount FROM buildjobexecutions LEFT JOIN buildjobs ON buildjobs.uuid=buildjobexecutions.jobuuid WHERE buildjobexecutions.jobuuid=? AND buildjobexecutions.active=?" + sJournalQuery + " ORDER BY buildjobexecutions.timestamp DESC";
+ std::string sSelectQuery = CBuildJobExecution::selectQueryWithoutConditions() + " WHERE buildjobexecutions.jobuuid=? AND buildjobexecutions.active=?" + sJournalQuery + " ORDER BY buildjobexecutions.timestamp DESC";
auto pStatement = m_pSQLHandler->prepareStatement(sSelectQuery);
pStatement->setString(1, m_sUUID);
pStatement->setInt(2, 1);
@@ -732,16 +748,7 @@ IBuildJobExecutionIterator* CBuildJob::RetrieveBuildJobExecutions(const std::str
std::unique_ptr buildJobExecutionIterator(new CBuildJobExecutionIterator());
while (pStatement->nextRow()) {
- // ATTENTION: All callers of makeFromStatement MUST BE in Sync in terms of column order!!
- // Column 1: Execution UUID
- // Column 2: Job UUID
- // Column 3: Journal UUID
- // Column 4: User UUID
- // Column 5: Start TimeStamp
- // Column 6: Job Name
- // Column 7: Job Status
- // Column 8: Job Layer Count
-
+ // Columns: see CBuildJobExecution::selectQueryWithoutConditions
buildJobExecutionIterator->AddJobExecution(CBuildJobExecution::makeSharedFromStatement(m_pSQLHandler, pStatement, m_pStorageState));
}
@@ -754,7 +761,7 @@ IBuildJobExecutionIterator* CBuildJob::RetrieveBuildJobExecutionsByStatus(const
if (!sJournalUUIDFilter.empty())
sJournalQuery = " AND buildjobexecutions.journaluuid=?";
- std::string sSelectQuery = "SELECT buildjobexecutions.uuid, buildjobexecutions.jobuuid, buildjobexecutions.journaluuid, buildjobexecutions.useruuid, buildjobexecutions.startjournaltimestamp, buildjobs.name, buildjobs.status, buildjobs.layercount FROM buildjobexecutions LEFT JOIN buildjobs ON buildjobs.uuid=buildjobexecutions.jobuuid WHERE buildjobexecutions.jobuuid=? AND buildjobexecutions.active=? AND buildjobexecutions.status=?" + sJournalQuery + " ORDER BY buildjobexecutions.timestamp DESC";
+ std::string sSelectQuery = CBuildJobExecution::selectQueryWithoutConditions() + " WHERE buildjobexecutions.jobuuid=? AND buildjobexecutions.active=? AND buildjobexecutions.status=?" + sJournalQuery + " ORDER BY buildjobexecutions.timestamp DESC";
auto pStatement = m_pSQLHandler->prepareStatement(sSelectQuery);
pStatement->setString(1, m_sUUID);
@@ -766,16 +773,7 @@ IBuildJobExecutionIterator* CBuildJob::RetrieveBuildJobExecutionsByStatus(const
std::unique_ptr buildJobExecutionIterator(new CBuildJobExecutionIterator());
while (pStatement->nextRow()) {
- // ATTENTION: All callers of makeFromStatement MUST BE in Sync in terms of column order!!
- // Column 1: Execution UUID
- // Column 2: Job UUID
- // Column 3: Journal UUID
- // Column 4: User UUID
- // Column 5: Start TimeStamp
- // Column 6: Job Name
- // Column 7: Job Status
- // Column 8: Job Layer Count
-
+ // Columns: see CBuildJobExecution::selectQueryWithoutConditions
buildJobExecutionIterator->AddJobExecution(CBuildJobExecution::makeSharedFromStatement(m_pSQLHandler, pStatement, m_pStorageState));
}
diff --git a/Implementation/LibMCData/libmcdata_buildjob.hpp b/Implementation/LibMCData/libmcdata_buildjob.hpp
index 461d1c31a..bfee02a13 100644
--- a/Implementation/LibMCData/libmcdata_buildjob.hpp
+++ b/Implementation/LibMCData/libmcdata_buildjob.hpp
@@ -178,6 +178,8 @@ class CBuildJob : public virtual IBuildJob, public virtual CBase {
static std::string convertBuildJobStatusToString(const LibMCData::eBuildJobStatus eStatus);
static LibMCData::eBuildJobStatus convertStringToBuildJobStatus(const std::string& sValue);
+ // True unless the (normalized) thumbnail stream UUID is the empty UUID.
+ static bool thumbnailStreamUUIDIsSet(const std::string& sThumbnailStreamUUID);
diff --git a/Implementation/LibMCData/libmcdata_buildjobexecution.cpp b/Implementation/LibMCData/libmcdata_buildjobexecution.cpp
index b02387054..9a7fca0f9 100644
--- a/Implementation/LibMCData/libmcdata_buildjobexecution.cpp
+++ b/Implementation/LibMCData/libmcdata_buildjobexecution.cpp
@@ -47,7 +47,7 @@ using namespace LibMCData::Impl;
Class definition of CBuildJobExecution
**************************************************************************************************************************/
-CBuildJobExecution::CBuildJobExecution(AMCData::PSQLHandler pSQLHandler, const std::string& sExecutionUUID, const std::string& sJobUUID, const std::string& sJournalUUID, const std::string& sUserUUID, uint64_t nStartJournalTimeStamp, const std::string& sJobName, eBuildJobStatus jobStatus, uint32_t nJobLayerCount, AMCData::PStorageState pStorageState)
+CBuildJobExecution::CBuildJobExecution(AMCData::PSQLHandler pSQLHandler, const std::string& sExecutionUUID, const std::string& sJobUUID, const std::string& sJournalUUID, const std::string& sUserUUID, uint64_t nStartJournalTimeStamp, const sBuildJobExecutionLoadedJob& loadedJob, const sBuildJobExecutionLoadedValues& loadedValues, AMCData::PStorageState pStorageState)
: m_pSQLHandler (pSQLHandler),
m_sExecutionUUID (AMCCommon::CUtils::normalizeUUIDString (sExecutionUUID)),
m_pStorageState (pStorageState),
@@ -55,9 +55,8 @@ CBuildJobExecution::CBuildJobExecution(AMCData::PSQLHandler pSQLHandler, const s
m_sJournalUUID(AMCCommon::CUtils::normalizeUUIDString(sJournalUUID)),
m_sUserUUID(AMCCommon::CUtils::normalizeUUIDString(sUserUUID)),
m_nStartJournalTimestamp (nStartJournalTimeStamp),
- m_sJobName (sJobName),
- m_JobStatus (jobStatus),
- m_nJobLayerCount (nJobLayerCount)
+ m_LoadedJob (loadedJob),
+ m_LoadedValues (loadedValues)
{
if (pSQLHandler.get() == nullptr)
throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDPARAM);
@@ -71,17 +70,52 @@ CBuildJobExecution::~CBuildJobExecution()
}
-CBuildJobExecution* CBuildJobExecution::makeFromStatement(AMCData::PSQLHandler pSQLHandler, AMCData::PSQLStatement pStatement, AMCData::PStorageState pStorageState)
+std::string CBuildJobExecution::selectQueryWithoutConditions()
{
- // ATTENTION: All callers of makeFromStatement MUST BE in Sync in terms of column order!!
// Column 1: Execution UUID
// Column 2: Job UUID
// Column 3: Journal UUID
// Column 4: User UUID
// Column 5: Start TimeStamp
- // Column 6: Job Name
- // Column 7: Job Status
- // Column 8: Job Layer Count
+ // Column 6: Job Name (NULL if the build job row is missing)
+ // Column 7: Job Status (NULL if the build job row is missing)
+ // Column 8: Job Layer Count (NULL if the build job row is missing)
+ // Column 9: Execution Status
+ // Column 10: Execution Description
+ // Column 11: Execution End TimeStamp
+ // Column 12: Job Thumbnail UUID (NULL if the build job row is missing)
+ return "SELECT buildjobexecutions.uuid, buildjobexecutions.jobuuid, buildjobexecutions.journaluuid, buildjobexecutions.useruuid, buildjobexecutions.startjournaltimestamp, "
+ "buildjobs.name, buildjobs.status, buildjobs.layercount, "
+ "buildjobexecutions.status, buildjobexecutions.description, buildjobexecutions.endjournaltimestamp, buildjobs.thumbnailuuid "
+ "FROM buildjobexecutions LEFT JOIN buildjobs ON buildjobs.uuid=buildjobexecutions.jobuuid";
+}
+
+// A damaged parent job row degrades the execution instead of failing every list that contains it:
+// an unknown status means the job can no longer be used, so it is reported like a missing row.
+static LibMCData::eBuildJobStatus jobStatusOrDeleted(const std::string& sJobStatus)
+{
+ try {
+ return CBuildJob::convertStringToBuildJobStatus(sJobStatus);
+ }
+ catch (ELibMCDataInterfaceException&) {
+ return LibMCData::eBuildJobStatus::Deleted;
+ }
+}
+
+// A malformed thumbnail UUID is reported as no thumbnail.
+static std::string thumbnailUUIDOrEmpty(const std::string& sThumbnailUUID)
+{
+ try {
+ return AMCCommon::CUtils::normalizeUUIDString(sThumbnailUUID);
+ }
+ catch (std::exception&) {
+ return AMCCommon::CUtils::createEmptyUUID();
+ }
+}
+
+CBuildJobExecution* CBuildJobExecution::makeFromStatement(AMCData::PSQLHandler pSQLHandler, AMCData::PSQLStatement pStatement, AMCData::PStorageState pStorageState)
+{
+ // ATTENTION: The statement MUST select the columns of selectQueryWithoutConditions()
if (pStatement.get() == nullptr)
throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDPARAM);
@@ -98,13 +132,29 @@ CBuildJobExecution* CBuildJobExecution::makeFromStatement(AMCData::PSQLHandler p
uint64_t nStartJournalTimestamp = (uint64_t)nStartTimeStamp;
- std::string sJobName = pStatement->getColumnString(6);
- eBuildJobStatus jobStatus = CBuildJob::convertStringToBuildJobStatus (pStatement->getColumnString(7));
- int32_t nLayerCount = pStatement->getColumnInt(8);
- if (nLayerCount < 0)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBLAYERCOUNT, "invalid build job layer count: " + sJobUUID);
+ // The build job row is never deleted by LibMCData, but if it is missing the execution
+ // is reported as belonging to a deleted job instead of failing the whole list.
+ sBuildJobExecutionLoadedJob loadedJob;
+ loadedJob.m_Status = LibMCData::eBuildJobStatus::Deleted;
+ loadedJob.m_nLayerCount = 0;
+ loadedJob.m_sThumbnailStreamUUID = AMCCommon::CUtils::createEmptyUUID();
+ if (!pStatement->columnIsNull(7)) {
+ loadedJob.m_sName = pStatement->getColumnString(6);
+ loadedJob.m_Status = jobStatusOrDeleted(pStatement->getColumnString(7));
+ int32_t nLayerCount = pStatement->getColumnInt(8);
+ if (nLayerCount < 0)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBLAYERCOUNT, "invalid build job layer count: " + sJobUUID);
+ loadedJob.m_nLayerCount = (uint32_t)nLayerCount;
+ if (!pStatement->columnIsNull(12))
+ loadedJob.m_sThumbnailStreamUUID = thumbnailUUIDOrEmpty(pStatement->getColumnString(12));
+ }
+
+ sBuildJobExecutionLoadedValues loadedValues;
+ loadedValues.m_Status = convertStringToBuildJobExecutionStatus(pStatement->getColumnString(9));
+ loadedValues.m_sDescription = pStatement->getColumnString(10);
+ loadedValues.m_EndTimeStamp = parseEndTimeStamp(pStatement->getColumnString(11));
- auto pExecution = std::make_unique(pSQLHandler, sExecutionUUID, sJobUUID, sJournalUUID, sUserUUID, nStartJournalTimestamp, sJobName, jobStatus, (uint32_t) nLayerCount, pStorageState);
+ auto pExecution = std::make_unique(pSQLHandler, sExecutionUUID, sJobUUID, sJournalUUID, sUserUUID, nStartJournalTimestamp, loadedJob, loadedValues, pStorageState);
return pExecution.release();
}
@@ -118,23 +168,13 @@ CBuildJobExecution* CBuildJobExecution::makeFromDatabase(AMCData::PSQLHandler pS
throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDPARAM);
// Retrieve non-mutable values at the beginning
- std::string sSelectQuery = "SELECT buildjobexecutions.uuid, buildjobexecutions.jobuuid, buildjobexecutions.journaluuid, buildjobexecutions.useruuid, buildjobexecutions.startjournaltimestamp, buildjobs.name, buildjobs.status, buildjobs.layercount FROM buildjobexecutions LEFT JOIN buildjobs ON buildjobs.uuid=buildjobexecutions.jobuuid WHERE buildjobexecutions.active=1 AND buildjobexecutions.uuid=?";
+ std::string sSelectQuery = selectQueryWithoutConditions() + " WHERE buildjobexecutions.active=1 AND buildjobexecutions.uuid=?";
auto pSelectStatement = pSQLHandler->prepareStatement(sSelectQuery);
pSelectStatement->setString(1, sNormalizedExecutionUUID);
if (!pSelectStatement->nextRow())
throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONNOTFOUND, "build job execution not found: " + sNormalizedExecutionUUID);
- // ATTENTION: All callers of makeFromStatement MUST BE in Sync in terms of column order!!
- // Column 1: Execution UUID
- // Column 2: Job UUID
- // Column 3: Journal UUID
- // Column 4: User UUID
- // Column 5: Start TimeStamp
- // Column 6: Job Name
- // Column 7: Job Status
- // Column 8: Job Layer Count
-
return makeFromStatement(pSQLHandler, pSelectStatement, pStorageState);
}
@@ -166,7 +206,7 @@ CBuildJobExecution* CBuildJobExecution::makeFrom(CBuildJobExecution* pBuildJobEx
if (pBuildJobExecution == nullptr)
throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDPARAM);
- return new CBuildJobExecution(pBuildJobExecution->m_pSQLHandler, pBuildJobExecution->m_sExecutionUUID, pBuildJobExecution->m_sJobUUID, pBuildJobExecution->m_sJournalUUID, pBuildJobExecution->m_sUserUUID, pBuildJobExecution->m_nStartJournalTimestamp, pBuildJobExecution->m_sJobName, pBuildJobExecution->m_JobStatus, pBuildJobExecution->m_nJobLayerCount, pBuildJobExecution->m_pStorageState);
+ return new CBuildJobExecution(pBuildJobExecution->m_pSQLHandler, pBuildJobExecution->m_sExecutionUUID, pBuildJobExecution->m_sJobUUID, pBuildJobExecution->m_sJournalUUID, pBuildJobExecution->m_sUserUUID, pBuildJobExecution->m_nStartJournalTimestamp, pBuildJobExecution->m_LoadedJob, pBuildJobExecution->m_LoadedValues, pBuildJobExecution->m_pStorageState);
}
std::shared_ptr CBuildJobExecution::makeSharedFrom(CBuildJobExecution* pBuildJobExecution)
@@ -187,22 +227,22 @@ std::string CBuildJobExecution::GetJobUUID()
std::string CBuildJobExecution::GetJobName()
{
- return m_sJobName;
+ return m_LoadedJob.m_sName;
}
LibMCData::eBuildJobStatus CBuildJobExecution::GetJobStatus()
{
- return m_JobStatus;
+ return m_LoadedJob.m_Status;
}
std::string CBuildJobExecution::GetJobStatusString()
{
- return CBuildJob::convertBuildJobStatusToString (m_JobStatus);
+ return CBuildJob::convertBuildJobStatusToString (m_LoadedJob.m_Status);
}
LibMCData_uint32 CBuildJobExecution::GetJobLayerCount()
{
- return m_nJobLayerCount;
+ return m_LoadedJob.m_nLayerCount;
}
@@ -319,23 +359,47 @@ LibMCData_uint64 CBuildJobExecution::GetEndTimeStampInMicroseconds()
throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONNOTFOUND, "build job execution not found: " + m_sExecutionUUID);
auto eStatus = convertStringToBuildJobExecutionStatus(pSelectStatement->getColumnString(1));
+ return endTimeStampFromValues(m_sExecutionUUID, m_nStartJournalTimestamp, eStatus, parseEndTimeStamp(pSelectStatement->getColumnString(2)));
+}
+
+std::optional CBuildJobExecution::parseEndTimeStamp(const std::string& sEndTimeStamp)
+{
+ uint64_t nEndTimeStamp;
+ try {
+ nEndTimeStamp = AMCCommon::CChrono::parseISO8601TimeUTC(sEndTimeStamp);
+ }
+ catch (std::exception&) {
+ // In-process executions store 0, which is no ISO8601 string.
+ return std::nullopt;
+ }
+
+ if (!AMCCommon::CChrono::timeStampIsWithinAMillionYears(nEndTimeStamp))
+ return std::nullopt;
+
+ return nEndTimeStamp;
+}
+
+bool CBuildJobExecution::isFromCurrentJournal()
+{
+ return m_sJournalUUID == m_pStorageState->getSessionUUID();
+}
+
+uint64_t CBuildJobExecution::endTimeStampFromValues(const std::string& sExecutionUUID, uint64_t nStartTimeStamp, eBuildJobExecutionStatus eStatus, const std::optional& endTimeStamp)
+{
if ((eStatus != eBuildJobExecutionStatus::Finished) && (eStatus != eBuildJobExecutionStatus::Failed))
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONENDNOTAVAILABLE, "build job execution end is not available: " + m_sExecutionUUID);
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONENDNOTAVAILABLE, "build job execution end is not available: " + sExecutionUUID);
- int64_t nEndTimeStamp = AMCCommon::CChrono::parseISO8601TimeUTC (pSelectStatement->getColumnString(2));
- if (nEndTimeStamp < 0)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBEXECUTIONEND, "invalid build job execution end: " + m_sExecutionUUID);
- if (!AMCCommon::CChrono::timeStampIsWithinAMillionYears((uint64_t)nEndTimeStamp))
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBEXECUTIONEND, "invalid build job execution end: " + m_sExecutionUUID);
+ if (!endTimeStamp.has_value())
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBEXECUTIONEND, "invalid build job execution end: " + sExecutionUUID);
- if ((uint64_t) (nEndTimeStamp) < m_nStartJournalTimestamp)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONENDISBEFORESTART, "Build job execution end is before start: " + m_sExecutionUUID);
+ if (*endTimeStamp < nStartTimeStamp)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONENDISBEFORESTART, "Build job execution end is before start: " + sExecutionUUID);
- return (uint64_t)nEndTimeStamp;
+ return *endTimeStamp;
}
-LibMCData_uint64 CBuildJobExecution::ComputeElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure)
+LibMCData_uint64 CBuildJobExecution::ComputeElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure)
{
if (!AMCCommon::CChrono::timeStampIsWithinAMillionYears(nGlobalTimerInMicroseconds)) {
@@ -357,64 +421,103 @@ LibMCData_uint64 CBuildJobExecution::ComputeElapsedTimeInMicroseconds(const LibM
}
auto eStatus = convertStringToBuildJobExecutionStatus(pSelectStatement->getColumnString(1));
+ return elapsedTimeFromValues(m_sExecutionUUID, m_nStartJournalTimestamp, isFromCurrentJournal(), eStatus, parseEndTimeStamp(pSelectStatement->getColumnString(2)), nGlobalTimerInMicroseconds, bThrowExceptionInFailure);
+}
+
+uint64_t CBuildJobExecution::elapsedTimeFromValues(const std::string& sExecutionUUID, uint64_t nStartTimeStamp, bool bIsFromCurrentJournal, eBuildJobExecutionStatus eStatus, const std::optional& endTimeStamp, const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure)
+{
+ if (!AMCCommon::CChrono::timeStampIsWithinAMillionYears(nGlobalTimerInMicroseconds)) {
+ if (bThrowExceptionInFailure)
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDPARAM);
+
+ return 0;
+ }
switch (eStatus) {
case eBuildJobExecutionStatus::InProcess:
{
- if (m_sJournalUUID != m_pStorageState->getSessionUUID()) {
+ if (!bIsFromCurrentJournal) {
if (bThrowExceptionInFailure)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONISFROMPASTJOURNAL, "build job execution is from past journal: " + m_sExecutionUUID);
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONISFROMPASTJOURNAL, "build job execution is from past journal: " + sExecutionUUID);
return 0;
}
- if (nGlobalTimerInMicroseconds < m_nStartJournalTimestamp) {
+ if (nGlobalTimerInMicroseconds < nStartTimeStamp) {
if (bThrowExceptionInFailure)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONSTARTISINTHEFUTURE, "build job execution start is in the future: " + m_sExecutionUUID);
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONSTARTISINTHEFUTURE, "build job execution start is in the future: " + sExecutionUUID);
return 0;
}
-
- return nGlobalTimerInMicroseconds - m_nStartJournalTimestamp;
+
+ return nGlobalTimerInMicroseconds - nStartTimeStamp;
}
case eBuildJobExecutionStatus::Finished:
case eBuildJobExecutionStatus::Failed:
{
- int64_t nEndTimeStamp = AMCCommon::CChrono::parseISO8601TimeUTC(pSelectStatement->getColumnString(2));
- if (nEndTimeStamp < 0) {
+ if (!endTimeStamp.has_value()) {
if (bThrowExceptionInFailure)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBEXECUTIONEND, "invalid build job execution end: " + m_sExecutionUUID);
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBEXECUTIONEND, "invalid build job execution end: " + sExecutionUUID);
return 0;
}
- if (!AMCCommon::CChrono::timeStampIsWithinAMillionYears((uint64_t)nEndTimeStamp)) {
- if (bThrowExceptionInFailure)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBEXECUTIONEND, "invalid build job execution end: " + m_sExecutionUUID);
- return 0;
- }
-
- if ((uint64_t)(nEndTimeStamp) < m_nStartJournalTimestamp) {
+ if (*endTimeStamp < nStartTimeStamp) {
if (bThrowExceptionInFailure)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONENDISBEFORESTART, "Build job execution end is before start: " + m_sExecutionUUID);
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBEXECUTIONENDISBEFORESTART, "Build job execution end is before start: " + sExecutionUUID);
return 0;
}
- return (uint64_t)nEndTimeStamp - m_nStartJournalTimestamp;
+ return *endTimeStamp - nStartTimeStamp;
}
default:
if (bThrowExceptionInFailure)
- throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBDURATIONNOTAVAILABLE, "build job duration is not available: " + m_sExecutionUUID);
+ throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_BUILDJOBDURATIONNOTAVAILABLE, "build job duration is not available: " + sExecutionUUID);
return 0;
}
}
+LibMCData::eBuildJobExecutionStatus CBuildJobExecution::GetLoadedStatus()
+{
+ return m_LoadedValues.m_Status;
+}
+
+std::string CBuildJobExecution::GetLoadedStatusString()
+{
+ return convertBuildJobExecutionStatusToString(m_LoadedValues.m_Status);
+}
+
+std::string CBuildJobExecution::GetLoadedDescription()
+{
+ return m_LoadedValues.m_sDescription;
+}
+
+LibMCData_uint64 CBuildJobExecution::GetLoadedEndTimeStampInMicroseconds()
+{
+ return endTimeStampFromValues(m_sExecutionUUID, m_nStartJournalTimestamp, m_LoadedValues.m_Status, m_LoadedValues.m_EndTimeStamp);
+}
+
+LibMCData_uint64 CBuildJobExecution::ComputeLoadedElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure)
+{
+ return elapsedTimeFromValues(m_sExecutionUUID, m_nStartJournalTimestamp, isFromCurrentJournal(), m_LoadedValues.m_Status, m_LoadedValues.m_EndTimeStamp, nGlobalTimerInMicroseconds, bThrowExceptionInFailure);
+}
+
+bool CBuildJobExecution::JobHasThumbnailStream()
+{
+ return CBuildJob::thumbnailStreamUUIDIsSet(m_LoadedJob.m_sThumbnailStreamUUID);
+}
+
+std::string CBuildJobExecution::GetJobThumbnailStreamUUID()
+{
+ return m_LoadedJob.m_sThumbnailStreamUUID;
+}
+
std::string CBuildJobExecution::AddJobExecutionData(const std::string& sIdentifier, const std::string& sName, IStorageStream* pStream, const LibMCData::eCustomDataType eDataType, const std::string& sUserUUID, const LibMCData_uint64 nAbsoluteTimeStamp)
{
if (pStream == nullptr)
@@ -654,3 +757,4 @@ LibMCData::eBuildJobExecutionStatus CBuildJobExecution::convertStringToBuildJobE
throw ELibMCDataInterfaceException(LIBMCDATA_ERROR_INVALIDBUILDJOBEXECUTIONSTATUS);
}
+
diff --git a/Implementation/LibMCData/libmcdata_buildjobexecution.hpp b/Implementation/LibMCData/libmcdata_buildjobexecution.hpp
index 2b0abc5ce..1d451b8bb 100644
--- a/Implementation/LibMCData/libmcdata_buildjobexecution.hpp
+++ b/Implementation/LibMCData/libmcdata_buildjobexecution.hpp
@@ -36,6 +36,7 @@ Abstract: This is the class declaration of CBuildJobExecution
#define __LIBMCDATA_BUILDJOBEXECUTION
#include
+#include
#include "libmcdata_interfaces.hpp"
#include "libmcdata_buildjobexecutiondata.hpp"
@@ -55,7 +56,29 @@ namespace Impl {
/*************************************************************************************************************************
- Class declaration of CBuildJobExecution
+ Parent build job values, read together with the execution (LEFT JOIN on buildjobs).
+**************************************************************************************************************************/
+
+struct sBuildJobExecutionLoadedJob {
+ std::string m_sName;
+ eBuildJobStatus m_Status;
+ uint32_t m_nLayerCount;
+ std::string m_sThumbnailStreamUUID;
+};
+
+/*************************************************************************************************************************
+ Execution values that are loaded together with a build job execution and returned by the GetLoaded* getters
+ without another database query. The other mutable getters (GetStatus, GetDescription, ...) keep reading fresh.
+**************************************************************************************************************************/
+
+struct sBuildJobExecutionLoadedValues {
+ eBuildJobExecutionStatus m_Status;
+ std::string m_sDescription;
+ std::optional m_EndTimeStamp; // empty if endjournaltimestamp is not a valid time stamp (as while in process)
+};
+
+/*************************************************************************************************************************
+ Class declaration of CBuildJobExecution
**************************************************************************************************************************/
class CBuildJobExecution : public virtual IBuildJobExecution, public virtual CBase {
@@ -70,18 +93,28 @@ class CBuildJobExecution : public virtual IBuildJobExecution, public virtual CBa
// Build Job related information
std::string m_sJobUUID;
- std::string m_sJobName;
- eBuildJobStatus m_JobStatus;
- uint32_t m_nJobLayerCount;
+ sBuildJobExecutionLoadedJob m_LoadedJob;
+
+ sBuildJobExecutionLoadedValues m_LoadedValues;
CBuildJobExecutionData* makeJobExecutionDataEx(AMCData::CSQLStatement* pStatement);
IBuildJobExecutionDataIterator* listJobExecutionDataEx(AMCData::CSQLStatement* pStatement);
+ // Parses an endjournaltimestamp column; empty if it is not a valid time stamp.
+ static std::optional parseEndTimeStamp(const std::string& sEndTimeStamp);
+ static uint64_t endTimeStampFromValues(const std::string& sExecutionUUID, uint64_t nStartTimeStamp, eBuildJobExecutionStatus eStatus, const std::optional& endTimeStamp);
+ static uint64_t elapsedTimeFromValues(const std::string& sExecutionUUID, uint64_t nStartTimeStamp, bool bIsFromCurrentJournal, eBuildJobExecutionStatus eStatus, const std::optional& endTimeStamp, const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure);
+
+ bool isFromCurrentJournal();
public:
static void bumpIncrementalID(AMCData::PSQLHandler pSQLHandler, const std::string& sExecutionUUID);
+ // "SELECT FROM buildjobexecutions LEFT JOIN buildjobs" in the column order makeFromStatement reads,
+ // without any condition. Callers append their WHERE and ORDER BY clauses.
+ static std::string selectQueryWithoutConditions();
+
static CBuildJobExecution* makeFrom(CBuildJobExecution * pBuildJobExecution);
static std::shared_ptr makeSharedFrom(CBuildJobExecution* pBuildJobExecution);
@@ -94,7 +127,7 @@ class CBuildJobExecution : public virtual IBuildJobExecution, public virtual CBa
static std::shared_ptr makeSharedFromStatement(AMCData::PSQLHandler pSQLHandler, AMCData::PSQLStatement pStatement, AMCData::PStorageState pStorageState);
- CBuildJobExecution(AMCData::PSQLHandler pSQLHandler, const std::string& sExecutionUUID, const std::string& sJobUUID, const std::string& sJournalUUID, const std::string& sUserUUID, uint64_t nStartJournalTimeStamp, const std::string & sJobName, eBuildJobStatus jobStatus, uint32_t nJobLayerCount, AMCData::PStorageState pStorageState);
+ CBuildJobExecution(AMCData::PSQLHandler pSQLHandler, const std::string& sExecutionUUID, const std::string& sJobUUID, const std::string& sJournalUUID, const std::string& sUserUUID, uint64_t nStartJournalTimeStamp, const sBuildJobExecutionLoadedJob& loadedJob, const sBuildJobExecutionLoadedValues& loadedValues, AMCData::PStorageState pStorageState);
virtual ~CBuildJobExecution();
@@ -130,6 +163,20 @@ class CBuildJobExecution : public virtual IBuildJobExecution, public virtual CBa
LibMCData_uint64 ComputeElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure);
+ LibMCData::eBuildJobExecutionStatus GetLoadedStatus() override;
+
+ std::string GetLoadedStatusString() override;
+
+ std::string GetLoadedDescription() override;
+
+ LibMCData_uint64 GetLoadedEndTimeStampInMicroseconds() override;
+
+ LibMCData_uint64 ComputeLoadedElapsedTimeInMicroseconds(const LibMCData_uint64 nGlobalTimerInMicroseconds, const bool bThrowExceptionInFailure) override;
+
+ bool JobHasThumbnailStream() override;
+
+ std::string GetJobThumbnailStreamUUID() override;
+
std::string AddJobExecutionData(const std::string& sIdentifier, const std::string& sName, IStorageStream* pStream, const LibMCData::eCustomDataType eDataType, const std::string& sUserUUID, const LibMCData_uint64 nAbsoluteTimeStamp) override;
IBuildJobExecutionDataIterator* ListJobExecutionDataByType(const LibMCData::eCustomDataType eDataType) override;
diff --git a/Implementation/LibMCData/libmcdata_buildjobhandler.cpp b/Implementation/LibMCData/libmcdata_buildjobhandler.cpp
index e8ab9b14f..f7471bdcb 100644
--- a/Implementation/LibMCData/libmcdata_buildjobhandler.cpp
+++ b/Implementation/LibMCData/libmcdata_buildjobhandler.cpp
@@ -230,7 +230,7 @@ IBuildJobExecution* CBuildJobHandler::RetrieveJobExecution(const std::string& sE
IBuildJobExecutionIterator* CBuildJobHandler::ListJobExecutions(const std::string& sMinTimestamp, const std::string& sMaxTimestamp, const std::string& sJournalUUIDFilter)
{
- std::string sQuery = "SELECT buildjobexecutions.uuid, buildjobexecutions.jobuuid, buildjobexecutions.journaluuid, buildjobexecutions.useruuid, buildjobexecutions.startjournaltimestamp, buildjobs.name, buildjobs.status, buildjobs.layercount FROM buildjobexecutions LEFT JOIN buildjobs ON buildjobs.uuid=buildjobexecutions.jobuuid WHERE buildjobexecutions.active=?";
+ std::string sQuery = CBuildJobExecution::selectQueryWithoutConditions() + " WHERE buildjobexecutions.active=?";
if (!sJournalUUIDFilter.empty())
sQuery += " AND buildjobexecutions.journaluuid=?";
if (!sMinTimestamp.empty ())
@@ -267,16 +267,7 @@ IBuildJobExecutionIterator* CBuildJobHandler::ListJobExecutions(const std::strin
std::unique_ptr buildJobIterator(new CBuildJobExecutionIterator());
while (pStatement->nextRow()) {
- // ATTENTION: All callers of makeFromStatement MUST BE in Sync in terms of column order!!
- // Column 1: Execution UUID
- // Column 2: Job UUID
- // Column 3: Journal UUID
- // Column 4: User UUID
- // Column 5: Start TimeStamp
- // Column 6: Job Name
- // Column 7: Job Status
- // Column 8: Job Layer Count
-
+ // Columns: see CBuildJobExecution::selectQueryWithoutConditions
buildJobIterator->AddJobExecution(CBuildJobExecution::makeSharedFromStatement(m_pSQLHandler, pStatement, m_pStorageState));
}
@@ -285,8 +276,7 @@ IBuildJobExecutionIterator* CBuildJobHandler::ListJobExecutions(const std::strin
IBuildJobExecutionIterator* CBuildJobHandler::ListJobExecutionsChangedSince(const LibMCData_uint64 nIncrementalID)
{
- // Column order must stay in sync with CBuildJobExecution::makeSharedFromStatement, see ListJobExecutions.
- std::string sQuery = "SELECT buildjobexecutions.uuid, buildjobexecutions.jobuuid, buildjobexecutions.journaluuid, buildjobexecutions.useruuid, buildjobexecutions.startjournaltimestamp, buildjobs.name, buildjobs.status, buildjobs.layercount FROM buildjobexecutions LEFT JOIN buildjobs ON buildjobs.uuid=buildjobexecutions.jobuuid WHERE buildjobexecutions.active=? AND buildjobexecutions.incremental_id>? ORDER BY buildjobexecutions.startjournaltimestamp DESC";
+ std::string sQuery = CBuildJobExecution::selectQueryWithoutConditions() + " WHERE buildjobexecutions.active=? AND buildjobexecutions.incremental_id>? ORDER BY buildjobexecutions.startjournaltimestamp DESC";
auto pStatement = m_pSQLHandler->prepareStatement(sQuery);
pStatement->setInt(1, 1);
@@ -294,6 +284,7 @@ IBuildJobExecutionIterator* CBuildJobHandler::ListJobExecutionsChangedSince(cons
std::unique_ptr buildJobIterator(new CBuildJobExecutionIterator());
while (pStatement->nextRow()) {
+ // Columns: see CBuildJobExecution::selectQueryWithoutConditions
buildJobIterator->AddJobExecution(CBuildJobExecution::makeSharedFromStatement(m_pSQLHandler, pStatement, m_pStorageState));
}
diff --git a/Implementation/UnitTest/amc_unittests_libmcdata_buildjobs.hpp b/Implementation/UnitTest/amc_unittests_libmcdata_buildjobs.hpp
index e5b53a3de..2e9d033c8 100644
--- a/Implementation/UnitTest/amc_unittests_libmcdata_buildjobs.hpp
+++ b/Implementation/UnitTest/amc_unittests_libmcdata_buildjobs.hpp
@@ -36,11 +36,17 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "common_utils.hpp"
#include "libmcdata_dynamic.hpp"
#include "amcdata_sqlhandler_sqlite.hpp"
+#include "amc_api_executionlistentry.hpp"
+#include "common_chrono.hpp"
+#include "RapidJSON/document.h"
#include
#include
#include
#include
+#include
+#include
+#include
namespace AMCUnitTest {
@@ -122,6 +128,13 @@ namespace AMCUnitTest {
// Metadata lookups must not scan the whole metadata table
registerTest("MetaDataLookupsUseIndex", "Job and execution metadata lookups are planned via an index", eUnitTestCategory::utMandatoryPass, std::bind(&CUnitTestGroup_LibMCData_BuildJobs::testMetaDataLookupsUseIndex, this));
registerTest("MetaDataIndicesAddedOnStart", "A database from before the metadata indices gets them on the next start", eUnitTestCategory::utMandatoryPass, std::bind(&CUnitTestGroup_LibMCData_BuildJobs::testMetaDataIndicesAddedOnStart, this));
+
+ // Build history list: values loaded by the single ListJobExecutions query
+ registerTest("ListExecsLoadedValues", "ListJobExecutions (and the other execution queries) load status, description, end, elapsed, thumbnail and deleted flag", eUnitTestCategory::utMandatoryPass, std::bind(&CUnitTestGroup_LibMCData_BuildJobs::testListExecsLoadedValues, this));
+ registerTest("ListExecsLoadedIsSnapshot", "Loaded getters keep list values, fresh getters read the database", eUnitTestCategory::utMandatoryPass, std::bind(&CUnitTestGroup_LibMCData_BuildJobs::testListExecsLoadedIsSnapshot, this));
+ registerTest("ListExecsMalformedJobRow", "Execution lists tolerate an unparseable job status and a malformed thumbnail UUID", eUnitTestCategory::utMandatoryPass, std::bind(&CUnitTestGroup_LibMCData_BuildJobs::testListExecsMalformedJobRow, this));
+ registerTest("ListExecsOrphanedExecution", "Execution lists tolerate executions whose build job row is missing", eUnitTestCategory::utMandatoryPass, std::bind(&CUnitTestGroup_LibMCData_BuildJobs::testListExecsOrphanedExecution, this));
+ registerTest("ListExecsJSONEntry", "/api/executions entry JSON: thumbnail omission, end time stamp and duration, from list-loaded values", eUnitTestCategory::utMandatoryPass, std::bind(&CUnitTestGroup_LibMCData_BuildJobs::testListExecsJSONEntry, this));
}
void initializeTests() override {
@@ -1290,6 +1303,553 @@ namespace AMCUnitTest {
assertTrue(pRestartedHandler->RetrieveJob(sJobUUID)->GetMetaDataString("jobkey") == "jobvalue", "job metadata still readable");
assertTrue(pRestartedHandler->RetrieveJobExecution(sExecUUID)->GetMetaDataString("execkey") == "execvalue", "execution metadata still readable");
}
+
+ // ============= Build history list (ListJobExecutions loaded values) =============
+
+ struct SExpectedExecution {
+ std::string m_sJobUUID;
+ std::string m_sJobName;
+ uint32_t m_nLayerCount;
+ LibMCData::eBuildJobExecutionStatus m_Status;
+ std::string m_sStatusString;
+ std::string m_sDescription;
+ uint64_t m_nStart;
+ uint64_t m_nEnd; // 0 while in process
+ uint64_t m_nExpectedElapsed;
+ bool m_bJobDeleted;
+ std::string m_sThumbnailUUID;
+ };
+
+ static constexpr uint64_t nSeedBaseTimeStamp = 1700000000000000ULL; // 2023-11-14, fixed
+ static constexpr uint64_t nSeedGlobalTimer = nSeedBaseTimeStamp + 100000000000ULL; // after every seeded start
+
+ bool endNotAvailableIsThrown(std::function callback)
+ {
+ try {
+ callback();
+ }
+ catch (LibMCData::ELibMCDataException& E) {
+ return E.getErrorCode() == LIBMCDATA_ERROR_BUILDJOBEXECUTIONENDNOTAVAILABLE;
+ }
+ return false;
+ }
+
+ // Loaded values of an execution that was not read through ListJobExecutions.
+ void assertLoadedValuesMatchSeed(LibMCData::PBuildJobExecution pExec, const SExpectedExecution& entry, const std::string& sContext)
+ {
+ std::string sExecUUID = pExec->GetExecutionUUID();
+ assertTrue(pExec->GetLoadedStatus() == entry.m_Status, sContext + ": loaded status " + sExecUUID);
+ assertTrue(pExec->GetLoadedDescription() == entry.m_sDescription, sContext + ": loaded description " + sExecUUID);
+ assertTrue(pExec->ComputeLoadedElapsedTimeInMicroseconds(nSeedGlobalTimer, true) == entry.m_nExpectedElapsed, sContext + ": loaded elapsed " + sExecUUID);
+ if (entry.m_nEnd != 0)
+ assertTrue(pExec->GetLoadedEndTimeStampInMicroseconds() == entry.m_nEnd, sContext + ": loaded end " + sExecUUID);
+ assertTrue(pExec->GetJobName() == entry.m_sJobName, sContext + ": job name " + sExecUUID);
+ assertTrue(pExec->GetJobLayerCount() == entry.m_nLayerCount, sContext + ": layer count " + sExecUUID);
+ assertTrue((pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Deleted) == entry.m_bJobDeleted, sContext + ": deleted flag " + sExecUUID);
+ assertTrue(pExec->GetJobThumbnailStreamUUID() == entry.m_sThumbnailUUID, sContext + ": thumbnail " + sExecUUID);
+ }
+
+ // Seeds 12 jobs with 5 executions each: finished, failed and in-process executions,
+ // jobs with and without thumbnail, and deleted jobs.
+ std::map seedBuildHistory(SBuildJobsFixture& fixture)
+ {
+ std::map expected;
+ const std::string sEmptyUUID = AMCCommon::CUtils::createEmptyUUID();
+
+ for (uint32_t nJob = 0; nJob < 12; nJob++) {
+ std::string sJobName = "Seed Job " + std::to_string(nJob);
+ uint32_t nLayerCount = 100 + nJob;
+ auto pJob = createValidatedJob(fixture, sJobName, nLayerCount);
+
+ std::string sThumbnailUUID = sEmptyUUID;
+ if (nJob % 3 == 0) {
+ sThumbnailUUID = createTestStream(fixture, "thumbnail " + std::to_string(nJob));
+ pJob->SetThumbnailStreamUUID(sThumbnailUUID);
+ }
+
+ std::vector jobExecutionUUIDs;
+ for (uint32_t nExec = 0; nExec < 5; nExec++) {
+ uint32_t nIndex = nJob * 5 + nExec;
+ SExpectedExecution entry;
+ entry.m_sJobUUID = pJob->GetUUID();
+ entry.m_sJobName = sJobName;
+ entry.m_nLayerCount = nLayerCount;
+ entry.m_sDescription = "Seed execution " + std::to_string(nJob) + "-" + std::to_string(nExec);
+ entry.m_nStart = nSeedBaseTimeStamp + (uint64_t)nIndex * 60000000ULL;
+ entry.m_sThumbnailUUID = sThumbnailUUID;
+ entry.m_bJobDeleted = false;
+
+ auto pExec = pJob->CreateBuildJobExecution(entry.m_sDescription, fixture.m_sUserUUID, entry.m_nStart);
+
+ uint64_t nDuration = 1000000ULL * (10 + nIndex);
+ switch (nIndex % 7) {
+ case 5:
+ entry.m_Status = LibMCData::eBuildJobExecutionStatus::Failed;
+ entry.m_sStatusString = "failed";
+ entry.m_nEnd = entry.m_nStart + nDuration;
+ entry.m_nExpectedElapsed = nDuration;
+ pExec->ChangeStatus(entry.m_Status, entry.m_nEnd);
+ break;
+ case 6:
+ // Started in the current journal session, still running.
+ entry.m_Status = LibMCData::eBuildJobExecutionStatus::InProcess;
+ entry.m_sStatusString = "inprocess";
+ entry.m_nEnd = 0;
+ entry.m_nExpectedElapsed = nSeedGlobalTimer - entry.m_nStart;
+ break;
+ default:
+ entry.m_Status = LibMCData::eBuildJobExecutionStatus::Finished;
+ entry.m_sStatusString = "finished";
+ entry.m_nEnd = entry.m_nStart + nDuration;
+ entry.m_nExpectedElapsed = nDuration;
+ pExec->ChangeStatus(entry.m_Status, entry.m_nEnd);
+ break;
+ }
+
+ jobExecutionUUIDs.push_back(pExec->GetExecutionUUID());
+ expected[pExec->GetExecutionUUID()] = entry;
+ }
+
+ bool bDeleted = (nJob % 2 == 1);
+ if (bDeleted) {
+ pJob->ArchiveJob();
+ pJob->DeleteJob();
+ }
+ for (auto& sExecutionUUID : jobExecutionUUIDs)
+ expected[sExecutionUUID].m_bJobDeleted = bDeleted;
+ }
+
+ return expected;
+ }
+
+ void testListExecsLoadedValues()
+ {
+ auto fixture = createFixture("list_execs_loaded");
+ auto expected = seedBuildHistory(fixture);
+ assertTrue(expected.size() == 60, "seeded 60 executions");
+
+ auto pIterator = fixture.m_pBuildJobHandler->ListJobExecutions("", "", "");
+ assertTrue(pIterator->Count() == 60, "list returns all executions");
+
+ uint64_t nPreviousStart = UINT64_MAX;
+ uint32_t nRows = 0, nDeletedRows = 0, nThumbnailRows = 0, nDeletedThumbnailRows = 0, nInProcessRows = 0;
+ while (pIterator->MoveNext()) {
+ auto pExec = pIterator->GetCurrentJobExecution();
+ std::string sExecUUID = pExec->GetExecutionUUID();
+ auto iter = expected.find(sExecUUID);
+ assertTrue(iter != expected.end(), "listed execution was seeded: " + sExecUUID);
+ auto& entry = iter->second;
+ nRows++;
+
+ // Newest first
+ assertTrue(pExec->GetStartTimeStampInMicroseconds() == entry.m_nStart, "start timestamp " + sExecUUID);
+ assertTrue(entry.m_nStart < nPreviousStart, "list is ordered by start descending");
+ nPreviousStart = entry.m_nStart;
+
+ // Loaded values match what was seeded
+ assertTrue(pExec->GetLoadedStatus() == entry.m_Status, "loaded status " + sExecUUID);
+ assertTrue(pExec->GetLoadedStatusString() == entry.m_sStatusString, "loaded status string " + sExecUUID);
+ assertTrue(pExec->GetLoadedDescription() == entry.m_sDescription, "loaded description " + sExecUUID);
+ assertTrue(pExec->ComputeLoadedElapsedTimeInMicroseconds(nSeedGlobalTimer, true) == entry.m_nExpectedElapsed, "loaded elapsed " + sExecUUID);
+ if (entry.m_nEnd != 0) {
+ assertTrue(pExec->GetLoadedEndTimeStampInMicroseconds() == entry.m_nEnd, "loaded end " + sExecUUID);
+ }
+ else {
+ nInProcessRows++;
+ assertTrue(endNotAvailableIsThrown([&]() { pExec->GetLoadedEndTimeStampInMicroseconds(); }), "loaded end not available while in process " + sExecUUID);
+ }
+ assertTrue(pExec->GetJobName() == entry.m_sJobName, "job name " + sExecUUID);
+ assertTrue(pExec->GetJobLayerCount() == entry.m_nLayerCount, "layer count " + sExecUUID);
+ assertTrue((pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Deleted) == entry.m_bJobDeleted, "deleted flag " + sExecUUID);
+ assertTrue(pExec->GetJobThumbnailStreamUUID() == entry.m_sThumbnailUUID, "thumbnail " + sExecUUID);
+ assertTrue(pExec->JobHasThumbnailStream() == (entry.m_sThumbnailUUID != AMCCommon::CUtils::createEmptyUUID()), "has thumbnail " + sExecUUID);
+
+ // Loaded values equal what the per-row queries used by the build history return
+ assertTrue(pExec->GetLoadedStatus() == pExec->GetStatus(), "loaded status equals fresh " + sExecUUID);
+ assertTrue(pExec->GetLoadedStatusString() == pExec->GetStatusString(), "loaded status string equals fresh " + sExecUUID);
+ assertTrue(pExec->GetLoadedDescription() == pExec->GetDescription(), "loaded description equals fresh " + sExecUUID);
+ assertTrue(pExec->ComputeLoadedElapsedTimeInMicroseconds(nSeedGlobalTimer, false) == pExec->ComputeElapsedTimeInMicroseconds(nSeedGlobalTimer, false), "loaded elapsed equals fresh " + sExecUUID);
+ if (entry.m_nEnd != 0)
+ assertTrue(pExec->GetLoadedEndTimeStampInMicroseconds() == pExec->GetEndTimeStampInMicroseconds(), "loaded end equals fresh " + sExecUUID);
+
+ auto pJob = fixture.m_pBuildJobHandler->RetrieveJob(pExec->GetJobUUID());
+ assertTrue(pJob->GetStatus() == pExec->GetJobStatus(), "job status equals RetrieveJob " + sExecUUID);
+ assertTrue(pJob->HasThumbnailStream() == pExec->JobHasThumbnailStream(), "has thumbnail equals RetrieveJob " + sExecUUID);
+ assertTrue(pJob->GetThumbnailStreamUUID() == pExec->GetJobThumbnailStreamUUID(), "thumbnail equals RetrieveJob " + sExecUUID);
+
+ if (entry.m_bJobDeleted)
+ nDeletedRows++;
+ if (pExec->JobHasThumbnailStream())
+ nThumbnailRows++;
+ if (entry.m_bJobDeleted && pExec->JobHasThumbnailStream())
+ nDeletedThumbnailRows++;
+ }
+
+ // The seed covers every case the build history renders
+ assertTrue(nRows == 60, "iterated all rows");
+ assertTrue(nDeletedRows == 30, "deleted job rows");
+ assertTrue(nThumbnailRows == 20, "thumbnail rows");
+ assertTrue(nDeletedThumbnailRows == 10, "deleted job rows that keep a thumbnail");
+ assertTrue(nInProcessRows > 0, "in-process rows");
+
+ // The other queries that build executions share the same SELECT: the incremental
+ // execution list (delta), RetrieveJobExecution and the per-job lists.
+ auto pDeltaIterator = fixture.m_pBuildJobHandler->ListJobExecutionsChangedSince(0);
+ assertTrue(pDeltaIterator->Count() == 60, "delta from 0 returns all executions");
+ while (pDeltaIterator->MoveNext()) {
+ auto pExec = pDeltaIterator->GetCurrentJobExecution();
+ auto iter = expected.find(pExec->GetExecutionUUID());
+ assertTrue(iter != expected.end(), "delta execution was seeded");
+ assertLoadedValuesMatchSeed(pExec, iter->second, "delta");
+ }
+
+ std::set jobUUIDs;
+ for (auto& iter : expected) {
+ auto pExec = fixture.m_pBuildJobHandler->RetrieveJobExecution(iter.first);
+ assertLoadedValuesMatchSeed(pExec, iter.second, "retrieved");
+ jobUUIDs.insert(iter.second.m_sJobUUID);
+ }
+
+ uint32_t nPerJobRows = 0, nPerJobFinishedRows = 0;
+ for (auto& sJobUUID : jobUUIDs) {
+ auto pJob = fixture.m_pBuildJobHandler->RetrieveJob(sJobUUID);
+ auto pJobExecIterator = pJob->RetrieveBuildJobExecutions("");
+ while (pJobExecIterator->MoveNext()) {
+ auto pExec = pJobExecIterator->GetCurrentJobExecution();
+ assertLoadedValuesMatchSeed(pExec, expected.at(pExec->GetExecutionUUID()), "per job");
+ nPerJobRows++;
+ }
+ auto pFinishedIterator = pJob->RetrieveBuildJobExecutionsByStatus(LibMCData::eBuildJobExecutionStatus::Finished, "");
+ while (pFinishedIterator->MoveNext()) {
+ auto pExec = pFinishedIterator->GetCurrentJobExecution();
+ assertTrue(pExec->GetLoadedStatus() == LibMCData::eBuildJobExecutionStatus::Finished, "per job by status: loaded status");
+ assertLoadedValuesMatchSeed(pExec, expected.at(pExec->GetExecutionUUID()), "per job by status");
+ nPerJobFinishedRows++;
+ }
+ }
+ assertTrue(nPerJobRows == 60, "per job lists return all executions");
+ assertTrue(nPerJobFinishedRows > 0, "per job by status returns finished executions");
+ }
+
+ void testListExecsLoadedIsSnapshot()
+ {
+ auto fixture = createFixture("list_execs_snapshot");
+
+ auto pJob = createValidatedJob(fixture, "Snapshot Job");
+ uint64_t nStart = nSeedBaseTimeStamp;
+ auto pWriter = pJob->CreateBuildJobExecution("before", fixture.m_sUserUUID, nStart);
+
+ auto pIterator = fixture.m_pBuildJobHandler->ListJobExecutions("", "", "");
+ assertTrue(pIterator->MoveNext(), "list has the execution");
+ auto pListed = pIterator->GetCurrentJobExecution();
+
+ pWriter->SetDescription("after");
+ pWriter->ChangeStatus(LibMCData::eBuildJobExecutionStatus::Finished, nStart + 5000000ULL);
+
+ // Loaded getters keep what the list query returned
+ assertTrue(pListed->GetLoadedDescription() == "before", "loaded description is a snapshot");
+ assertTrue(pListed->GetLoadedStatus() == LibMCData::eBuildJobExecutionStatus::InProcess, "loaded status is a snapshot");
+ assertTrue(endNotAvailableIsThrown([&]() { pListed->GetLoadedEndTimeStampInMicroseconds(); }), "loaded end is a snapshot");
+
+ // Existing getters still read from the database
+ assertTrue(pListed->GetDescription() == "after", "GetDescription reads fresh");
+ assertTrue(pListed->GetStatus() == LibMCData::eBuildJobExecutionStatus::Finished, "GetStatus reads fresh");
+ assertTrue(pListed->GetEndTimeStampInMicroseconds() == nStart + 5000000ULL, "GetEndTimeStamp reads fresh");
+ }
+
+ void testListExecsOrphanedExecution()
+ {
+ auto fixture = createFixture("list_execs_orphan");
+
+ auto pKeptJob = createValidatedJob(fixture, "Kept Job", 7);
+ pKeptJob->CreateBuildJobExecution("kept", fixture.m_sUserUUID, nSeedBaseTimeStamp);
+
+ auto pLostJob = createValidatedJob(fixture, "Lost Job", 9);
+ auto pLostExec = pLostJob->CreateBuildJobExecution("orphan", fixture.m_sUserUUID, nSeedBaseTimeStamp + 1000000ULL);
+ pLostExec->ChangeStatus(LibMCData::eBuildJobExecutionStatus::Finished, nSeedBaseTimeStamp + 3000000ULL);
+ std::string sOrphanUUID = pLostExec->GetExecutionUUID();
+
+ // Nothing in LibMCData removes a buildjobs row; simulate a database where it is gone.
+ {
+ AMCData::CSQLHandler_SQLite sqlHandler(fixture.m_sBasePath + "/buildjobs.db");
+ auto pStatement = sqlHandler.prepareStatement("DELETE FROM buildjobs WHERE uuid=?");
+ pStatement->setString(1, pLostJob->GetUUID());
+ pStatement->execute();
+ }
+
+ // The whole list must still load; the orphan is shown as a deleted job.
+ auto pIterator = fixture.m_pBuildJobHandler->ListJobExecutions("", "", "");
+ assertTrue(pIterator->Count() == 2, "both executions are listed");
+
+ bool bFoundOrphan = false;
+ bool bFoundKept = false;
+ while (pIterator->MoveNext()) {
+ auto pExec = pIterator->GetCurrentJobExecution();
+ if (pExec->GetExecutionUUID() == sOrphanUUID) {
+ bFoundOrphan = true;
+ assertTrue(pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Deleted, "orphan job reported as deleted");
+ assertTrue(pExec->GetJobName() == "", "orphan job has no name");
+ assertTrue(pExec->GetJobLayerCount() == 0, "orphan job has no layers");
+ assertFalse(pExec->JobHasThumbnailStream(), "orphan job has no thumbnail");
+ assertTrue(pExec->GetJobThumbnailStreamUUID() == AMCCommon::CUtils::createEmptyUUID(), "orphan thumbnail is the empty UUID");
+ assertTrue(pExec->GetLoadedDescription() == "orphan", "orphan description");
+ assertTrue(pExec->GetLoadedStatus() == LibMCData::eBuildJobExecutionStatus::Finished, "orphan status");
+ assertTrue(pExec->GetLoadedEndTimeStampInMicroseconds() == nSeedBaseTimeStamp + 3000000ULL, "orphan end");
+ }
+ else {
+ bFoundKept = true;
+ assertTrue(pExec->GetJobName() == "Kept Job", "kept job name");
+ assertTrue(pExec->GetJobLayerCount() == 7, "kept job layer count");
+ assertTrue(pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Validated, "kept job status");
+ }
+ }
+ assertTrue(bFoundOrphan && bFoundKept, "found both rows");
+
+ // Retrieving the orphan by UUID works as well
+ auto pRetrieved = fixture.m_pBuildJobHandler->RetrieveJobExecution(sOrphanUUID);
+ assertTrue(pRetrieved->GetJobStatus() == LibMCData::eBuildJobStatus::Deleted, "retrieved orphan reported as deleted");
+
+ // The incremental list must not freeze on it either.
+ bool bDeltaFoundOrphan = false;
+ auto pDeltaIterator = fixture.m_pBuildJobHandler->ListJobExecutionsChangedSince(0);
+ assertTrue(pDeltaIterator->Count() == 2, "both executions are in the delta");
+ while (pDeltaIterator->MoveNext()) {
+ auto pExec = pDeltaIterator->GetCurrentJobExecution();
+ if (pExec->GetExecutionUUID() == sOrphanUUID) {
+ bDeltaFoundOrphan = true;
+ assertTrue(pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Deleted, "delta orphan reported as deleted");
+ }
+ }
+ assertTrue(bDeltaFoundOrphan, "delta contains the orphan");
+ }
+
+ void corruptBuildJobColumn(SBuildJobsFixture& fixture, const std::string& sJobUUID, const std::string& sColumn, const std::string& sValue)
+ {
+ // LibMCData never writes such values; simulate a database that was edited or damaged by hand.
+ AMCData::CSQLHandler_SQLite sqlHandler(fixture.m_sBasePath + "/buildjobs.db");
+ auto pStatement = sqlHandler.prepareStatement("UPDATE buildjobs SET " + sColumn + "=? WHERE uuid=?");
+ pStatement->setString(1, sValue);
+ pStatement->setString(2, sJobUUID);
+ pStatement->execute();
+ }
+
+ void testListExecsMalformedJobRow()
+ {
+ auto fixture = createFixture("list_execs_malformed");
+
+ auto pKeptJob = createValidatedJob(fixture, "Kept Job", 7);
+ std::string sKeptThumbnailUUID = createTestStream(fixture, "kept thumbnail");
+ pKeptJob->SetThumbnailStreamUUID(sKeptThumbnailUUID);
+ std::string sKeptExecUUID = pKeptJob->CreateBuildJobExecution("kept", fixture.m_sUserUUID, nSeedBaseTimeStamp)->GetExecutionUUID();
+
+ auto pBadStatusJob = createValidatedJob(fixture, "Bad Status Job", 8);
+ std::string sBadStatusExecUUID = pBadStatusJob->CreateBuildJobExecution("bad status", fixture.m_sUserUUID, nSeedBaseTimeStamp + 1000000ULL)->GetExecutionUUID();
+ corruptBuildJobColumn(fixture, pBadStatusJob->GetUUID(), "status", "no-such-status");
+
+ auto pBadThumbnailJob = createValidatedJob(fixture, "Bad Thumbnail Job", 9);
+ std::string sBadThumbnailExecUUID = pBadThumbnailJob->CreateBuildJobExecution("bad thumbnail", fixture.m_sUserUUID, nSeedBaseTimeStamp + 2000000ULL)->GetExecutionUUID();
+ corruptBuildJobColumn(fixture, pBadThumbnailJob->GetUUID(), "thumbnailuuid", "");
+
+ // One malformed job row must not fail the whole list.
+ auto pIterator = fixture.m_pBuildJobHandler->ListJobExecutions("", "", "");
+ assertTrue(pIterator->Count() == 3, "all executions are listed");
+
+ std::set foundUUIDs;
+ while (pIterator->MoveNext()) {
+ auto pExec = pIterator->GetCurrentJobExecution();
+ std::string sExecUUID = pExec->GetExecutionUUID();
+ foundUUIDs.insert(sExecUUID);
+
+ if (sExecUUID == sBadStatusExecUUID) {
+ // Same as a missing job row: the job can no longer be used, so it is shown as deleted.
+ assertTrue(pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Deleted, "unparseable job status reported as deleted");
+ assertTrue(pExec->GetJobName() == "Bad Status Job", "job name kept despite bad status");
+ assertTrue(pExec->GetJobLayerCount() == 8, "layer count kept despite bad status");
+ }
+ else if (sExecUUID == sBadThumbnailExecUUID) {
+ // Only the thumbnail is lost; the rest of the row is unaffected.
+ assertFalse(pExec->JobHasThumbnailStream(), "malformed thumbnail reported as no thumbnail");
+ assertTrue(pExec->GetJobThumbnailStreamUUID() == AMCCommon::CUtils::createEmptyUUID(), "malformed thumbnail is the empty UUID");
+ assertTrue(pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Validated, "job status kept despite bad thumbnail");
+ assertTrue(pExec->GetJobName() == "Bad Thumbnail Job", "job name kept despite bad thumbnail");
+ }
+ else {
+ assertTrue(sExecUUID == sKeptExecUUID, "third row is the intact one");
+ assertTrue(pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Validated, "intact job status");
+ assertTrue(pExec->GetJobThumbnailStreamUUID() == AMCCommon::CUtils::normalizeUUIDString(sKeptThumbnailUUID), "intact thumbnail");
+ }
+ }
+ assertTrue(foundUUIDs.size() == 3, "found all rows");
+
+ // The incremental list must not freeze on them either.
+ auto pDeltaIterator = fixture.m_pBuildJobHandler->ListJobExecutionsChangedSince(0);
+ assertTrue(pDeltaIterator->Count() == 3, "all executions are in the delta");
+ while (pDeltaIterator->MoveNext()) {
+ auto pExec = pDeltaIterator->GetCurrentJobExecution();
+ if (pExec->GetExecutionUUID() == sBadStatusExecUUID)
+ assertTrue(pExec->GetJobStatus() == LibMCData::eBuildJobStatus::Deleted, "delta: unparseable job status reported as deleted");
+ if (pExec->GetExecutionUUID() == sBadThumbnailExecUUID)
+ assertFalse(pExec->JobHasThumbnailStream(), "delta: malformed thumbnail reported as no thumbnail");
+ }
+ }
+ // Writes one listed execution the way /api/executions does and returns it as JSON text.
+ std::string writeListEntry(LibMCData::PBuildJobExecution pExecution, uint64_t nGlobalTimer)
+ {
+ AMC::CJSONWriter writer;
+ AMC::CJSONWriterObject entryObject(writer);
+ AMC::writeExecutionListEntryToJSON(entryObject, pExecution, nGlobalTimer);
+ writer.addObject("entry", entryObject);
+ return writer.saveToString();
+ }
+
+ struct SListEntry {
+ std::map m_Strings;
+ std::vector m_Keys;
+ int64_t m_nDuration = -1;
+ int64_t m_nLayerCount = -1;
+ };
+
+ SListEntry parseListEntry(const std::string& sJSON)
+ {
+ rapidjson::Document doc;
+ doc.Parse(sJSON.c_str());
+ assertTrue(doc.IsObject() && doc.HasMember("entry") && doc["entry"].IsObject(), "entry is a JSON object");
+
+ SListEntry entry;
+ for (auto& member : doc["entry"].GetObject()) {
+ entry.m_Keys.push_back(member.name.GetString());
+ if (member.value.IsString())
+ entry.m_Strings[member.name.GetString()] = member.value.GetString();
+ }
+ entry.m_nDuration = doc["entry"]["executionduration"].GetInt64();
+ entry.m_nLayerCount = doc["entry"]["executionlayercount"].GetInt64();
+ return entry;
+ }
+
+ void testListExecsJSONEntry()
+ {
+ auto fixture = createFixture("list_execs_json");
+ const std::vector baseKeys = { "executionuuid", "executionname", "executiondescription", "executionstarttimestamp",
+ "executionendtimestamp", "executionduration", "executionstatus", "executionbuildstatus", "executionlayercount", "jobuuid" };
+ std::vector thumbnailKeys = baseKeys;
+ thumbnailKeys.push_back("executionthumbnail");
+
+ // Deleted job that keeps its thumbnail, with a finished execution of 5 seconds.
+ auto pDeletedJob = createValidatedJob(fixture, "Bracket", 120);
+ std::string sDeletedThumbnailUUID = createTestStream(fixture, "bracket thumbnail");
+ pDeletedJob->SetThumbnailStreamUUID(sDeletedThumbnailUUID);
+ auto pFinished = pDeletedJob->CreateBuildJobExecution("first run", fixture.m_sUserUUID, nSeedBaseTimeStamp);
+ pFinished->ChangeStatus(LibMCData::eBuildJobExecutionStatus::Finished, nSeedBaseTimeStamp + 5000000ULL);
+ pDeletedJob->ArchiveJob();
+ pDeletedJob->DeleteJob();
+
+ // Job with thumbnail and a failed (aborted) execution of 7 seconds: it has an end time stamp too.
+ auto pFailedJob = createValidatedJob(fixture, "Gear", 90);
+ std::string sFailedThumbnailUUID = createTestStream(fixture, "gear thumbnail");
+ pFailedJob->SetThumbnailStreamUUID(sFailedThumbnailUUID);
+ auto pFailed = pFailedJob->CreateBuildJobExecution("aborted run", fixture.m_sUserUUID, nSeedBaseTimeStamp + 20000000ULL);
+ pFailed->ChangeStatus(LibMCData::eBuildJobExecutionStatus::Failed, nSeedBaseTimeStamp + 27000000ULL);
+
+ // Job without thumbnail, with an execution of this session that started 60 seconds before the timer.
+ auto pRunningJob = createValidatedJob(fixture, "Housing", 80);
+ auto pRunning = pRunningJob->CreateBuildJobExecution("second run", fixture.m_sUserUUID, nSeedBaseTimeStamp + 40000000ULL);
+ uint64_t nGlobalTimer = nSeedBaseTimeStamp + 100000000ULL;
+
+ // Execution whose job row is missing (damaged database).
+ auto pLostJob = createValidatedJob(fixture, "Lost", 70);
+ pLostJob->SetThumbnailStreamUUID(createTestStream(fixture, "lost thumbnail"));
+ auto pOrphan = pLostJob->CreateBuildJobExecution("orphan run", fixture.m_sUserUUID, nSeedBaseTimeStamp + 60000000ULL);
+ pOrphan->ChangeStatus(LibMCData::eBuildJobExecutionStatus::Finished, nSeedBaseTimeStamp + 63000000ULL);
+ {
+ AMCData::CSQLHandler_SQLite sqlHandler(fixture.m_sBasePath + "/buildjobs.db");
+ auto pStatement = sqlHandler.prepareStatement("DELETE FROM buildjobs WHERE uuid=?");
+ pStatement->setString(1, pLostJob->GetUUID());
+ pStatement->execute();
+ }
+
+ const std::string sNoEndTimeStamp = AMCCommon::CChrono::convertToISO8601TimeUTC(0);
+
+ std::map fullListJSON;
+ auto pIterator = fixture.m_pBuildJobHandler->ListJobExecutions("", "", "");
+ while (pIterator->MoveNext()) {
+ auto pExec = pIterator->GetCurrentJobExecution();
+ std::string sExecUUID = pExec->GetExecutionUUID();
+ std::string sJSON = writeListEntry(pExec, nGlobalTimer);
+ fullListJSON[sExecUUID] = sJSON;
+ auto entry = parseListEntry(sJSON);
+
+ assertTrue(entry.m_Strings["executionuuid"] == sExecUUID, "execution UUID");
+ assertTrue(entry.m_Strings["jobuuid"] == pExec->GetJobUUID(), "job UUID");
+ assertTrue(AMCCommon::CChrono::parseISO8601TimeUTC(entry.m_Strings["executionstarttimestamp"]) == (int64_t)pExec->GetStartTimeStampInMicroseconds(), "start time stamp");
+
+ if (sExecUUID == pFinished->GetExecutionUUID()) {
+ assertTrue(entry.m_Keys == thumbnailKeys, "finished: keys in order");
+ assertTrue(entry.m_Strings["executionname"] == "Bracket", "deleted job name");
+ assertTrue(entry.m_Strings["executionthumbnail"] == AMCCommon::CUtils::normalizeUUIDString(sDeletedThumbnailUUID), "deleted job keeps its thumbnail");
+ assertTrue(entry.m_Strings["executiondescription"] == "first run", "description");
+ assertTrue(entry.m_Strings["executionstatus"] == "finished", "execution status");
+ assertTrue(entry.m_Strings["executionbuildstatus"] == "deleted", "build status");
+ assertTrue(AMCCommon::CChrono::parseISO8601TimeUTC(entry.m_Strings["executionendtimestamp"]) == (int64_t)(nSeedBaseTimeStamp + 5000000ULL), "end time stamp");
+ assertTrue(entry.m_nDuration == 5, "duration in seconds");
+ assertTrue(entry.m_nLayerCount == 120, "layer count");
+ }
+ else if (sExecUUID == pFailed->GetExecutionUUID()) {
+ assertTrue(entry.m_Keys == thumbnailKeys, "failed: keys in order");
+ assertTrue(entry.m_Strings["executionname"] == "Gear", "job name");
+ assertTrue(entry.m_Strings["executionthumbnail"] == AMCCommon::CUtils::normalizeUUIDString(sFailedThumbnailUUID), "job thumbnail");
+ assertTrue(entry.m_Strings["executionstatus"] == "failed", "execution status");
+ assertTrue(entry.m_Strings["executionbuildstatus"] == "validated", "build status");
+ assertTrue(AMCCommon::CChrono::parseISO8601TimeUTC(entry.m_Strings["executionendtimestamp"]) == (int64_t)(nSeedBaseTimeStamp + 27000000ULL), "failed execution has an end time stamp");
+ assertTrue(entry.m_nDuration == 7, "failed duration in seconds");
+ assertTrue(entry.m_nLayerCount == 90, "layer count");
+ }
+ else if (sExecUUID == pRunning->GetExecutionUUID()) {
+ assertTrue(entry.m_Keys == baseKeys, "in process: no thumbnail key without job thumbnail");
+ assertTrue(entry.m_Strings["executionname"] == "Housing", "job name");
+ assertTrue(entry.m_Strings["executionstatus"] == "inprocess", "execution status");
+ assertTrue(entry.m_Strings["executionbuildstatus"] == "validated", "build status");
+ assertTrue(entry.m_Strings["executionendtimestamp"] == sNoEndTimeStamp, "no end time stamp while in process");
+ assertTrue(entry.m_nDuration == 60, "duration in seconds while in process");
+ assertTrue(entry.m_nLayerCount == 80, "layer count");
+ }
+ else {
+ assertTrue(sExecUUID == pOrphan->GetExecutionUUID(), "fourth row is the orphan");
+ assertTrue(entry.m_Keys == baseKeys, "orphan: no thumbnail key");
+ assertTrue(entry.m_Strings["executionname"] == "", "orphan has no job name");
+ assertTrue(entry.m_Strings["executionbuildstatus"] == "deleted", "orphan build status");
+ assertTrue(entry.m_Strings["executionstatus"] == "finished", "orphan execution status");
+ assertTrue(entry.m_Strings["executiondescription"] == "orphan run", "orphan description");
+ assertTrue(entry.m_nDuration == 3, "orphan duration");
+ assertTrue(entry.m_nLayerCount == 0, "orphan layer count");
+ }
+ }
+ assertTrue(fullListJSON.size() == 4, "all rows written");
+
+ // The delta path writes the same entries.
+ auto pDeltaIterator = fixture.m_pBuildJobHandler->ListJobExecutionsChangedSince(0);
+ uint32_t nDeltaRows = 0;
+ while (pDeltaIterator->MoveNext()) {
+ auto pExec = pDeltaIterator->GetCurrentJobExecution();
+ assertTrue(writeListEntry(pExec, nGlobalTimer) == fullListJSON.at(pExec->GetExecutionUUID()), "delta entry equals full list entry");
+ nDeltaRows++;
+ }
+ assertTrue(nDeltaRows == 4, "delta has all rows");
+
+ // The entry is written from the values loaded by the list query alone: changes made
+ // after listing do not show up, because the writer does not query the database.
+ auto pSnapshotIterator = fixture.m_pBuildJobHandler->ListJobExecutions("", "", "");
+ std::vector listedExecutions;
+ while (pSnapshotIterator->MoveNext())
+ listedExecutions.push_back(pSnapshotIterator->GetCurrentJobExecution());
+
+ pRunning->SetDescription("changed");
+ pRunning->ChangeStatus(LibMCData::eBuildJobExecutionStatus::Finished, nSeedBaseTimeStamp + 50000000ULL);
+ pRunningJob->ArchiveJob();
+ pFailedJob->SetThumbnailStreamUUID("");
+
+ for (auto& pExec : listedExecutions)
+ assertTrue(writeListEntry(pExec, nGlobalTimer) == fullListJSON.at(pExec->GetExecutionUUID()), "entry is written from list-loaded values");
+ }
};
}