Skip to content

Record runs as a store-agnostic record with a PROV view - #2

Open
vamsikalagaturu wants to merge 7 commits into
mainfrom
feat/prov-run-record
Open

vamsikalagaturu wants to merge 7 commits into
mainfrom
feat/prov-run-record

Conversation

@vamsikalagaturu

@vamsikalagaturu vamsikalagaturu commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor
  • RunRecord (rec.record) is what rec knows of a run: OSLC state and verdict, times, result and fail trace, program and recording software, trigger, host, agents, sources, repositories, dependencies, resources, artefacts and metrics
  • Observer(store) records every event once for any store through store.edit; only a run that has not started can start or be cancelled, and a step logged again replaces its value
  • A store is any object with load, edit, run_ids and close (rec.stores.Store); edit is atomic per run across processes, so a start and a cancel cannot both succeed; FileStore keeps a run as its PROV document (fmt="rdf") or its record as JSON (fmt="json") under an flock, MariaDBStore as one row with state and verdict columns under SELECT ... FOR UPDATE
  • MariaDBStore does not migrate tables created by MariaDBObserver
  • rec.provenance maps a record to PROV with the rdf-utils helpers and back: an unstarted run is a prov:Activity, a started one a prov-ext:Execution associated with rec whose plan is the file defining the run; run ids are percent-encoded in IRIs; a structured result is an rdf:JSON literal; files are entities, relative paths prov-ext:RelativeLocation under the directory they were logged from
  • Run records the program it ran, stops the heartbeat before any outcome (also on SystemExit), and runs post-run hooks after completion, so a hook that raises no longer rewrites the verdict
  • rdflib and rdf-utils become dependencies; MariaDB stays an extra; observers are replaced by the observer and stores

- RunRecord (rec.record) is what rec knows of a run: OSLC state and verdict, times, result and fail trace, program and recording software, trigger, host, agents, sources, repositories, dependencies, resources, artefacts and metrics
- Observer(store) records every event once for any store: load the record, change it, save it; a run cancelled in the store does not start, only a run that has not started can be cancelled, and a step logged again replaces its value
- A store is any object with load, save, run_ids and close (rec.stores.Store); FileStore keeps a run as its PROV document (fmt="rdf") or its record as JSON (fmt="json"), MariaDBStore as one row with state and verdict columns, migrating the previous release's table
- rec.provenance maps a record to PROV with the rdf-utils helpers and back: an unstarted run is a prov:Activity, a started one a prov-ext:Execution associated with rec whose plan is the file defining the run; files are entities, relative paths prov-ext:RelativeLocation under the directory they were logged from
- Run records the program it ran, stops the heartbeat before completion, and runs post-run hooks after it, so a hook that raises no longer rewrites the verdict
- rdflib and rdf-utils become dependencies; MariaDB stays an extra; observers are replaced by the observer and stores

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The dependency reference currently breaks imports, and lifecycle concurrency, migration, and PROV round-trip defects remain.

Review effort: Balanced
Findings: 3 High severity · 4 Medium severity

Open (7)
What changed in this PR

Introduces store-agnostic run records, JSON/PROV persistence, and MariaDB migration support.

Changes:

  • Adds RunRecord, lifecycle-aware observers, and pluggable stores.
  • Adds bidirectional PROV mapping and richer run metadata.
  • Replaces legacy observers and adds integration/conformance tests.
File Description
rec/​__init__.py Defines lifecycle states and verdicts.
rec/​record.py Defines the store-independent record model.
rec/​observer.py Applies run events to stored records.
rec/​run.py Records execution lifecycle and metadata.
rec/​provenance.py Converts records to and from PROV.
rec/​stores/​__init__.py Defines the store protocol.
rec/​stores/​file_store.py Adds JSON and RDF file storage.
rec/​stores/​mariadb_store.py Adds database storage and migration.
rec/​observers/​base.py Removes the legacy observer interface.
rec/​observers/​mariadb_observer.py Removes the legacy MariaDB observer.
rec/​observers/​__init__.py Retains the legacy package namespace.
tests/​test_run.py Tests lifecycle, PROV, and file stores.
tests/​test_mariadb_store.py Tests database storage and migration.
examples/​decentral_run.py Updates the MariaDB example.
README.md Documents records, stores, and tests.
pyproject.toml Updates dependencies and extras.
.github/​workflows/​tests.yml Adds Python and MariaDB CI coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pyproject.toml
"dotenv",
"mariadb"
"rdflib>=7.6.0",
"rdf-utils @ git+https://github.com/minhnh/rdf-utils@main",
Comment thread rec/observer.py Outdated
Comment thread rec/run.py Outdated
Comment on lines +198 to +200
if self.state is State.CANCELED:
raise RuntimeError("cannot start a cancelled run")
self._emit_started(trigger, starter)
Comment thread rec/provenance.py Outdated
Comment thread rec/provenance.py Outdated
Comment thread rec/provenance.py
Comment on lines +120 to +122
if record.start_time:
software = URIRef(f"{run}/software")
load_pkg_prov(g, software, record.recorder.name, record.recorder.version, record.recorder.commit, record.recorder.repository)
Comment thread rec/stores/mariadb_store.py Outdated
- Store.edit(run_id) replaces save: FileStore holds an flock on <id>.lock, MariaDBStore a SELECT ... FOR UPDATE transaction, so a start and a cancel from separate observers cannot both succeed
- A run starts only when new or queued, in Run.run and Observer.log_started_run
- Run ids are percent-encoded in run IRIs
- A list or mapping result is an rdf:JSON literal and reads back as itself
- Drop the migration of MariaDBObserver's table and its test
- Drop Run's unused ingredients, scenario and **kwargs
- The start refusal names the run's state, canceled, so the test matches "is canceled"

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

RDF round-tripping, legacy database migration, metric indexing, heartbeat cleanup, and dependency reproducibility have unresolved issues.

Review effort: Balanced
Findings: 4 High severity · 1 Medium severity

Open (5)
Resolved since last review (4)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Pin VCS dependency to a released or immutable version

pyproject.toml:11

Depending on the moving main branch makes installations and CI non-reproducible; an upstream change can break this package without any REC commit, especially because the code imports an underscore-prefixed helper from this dependency. Use a released version or pin the VCS reference to an immutable commit/tag.

Medium severity Select next step above the maximum existing metric step

rec/​observer.py:96

Counting existing metrics does not reliably select a new step. For example, after explicitly logging step 1, the next call without a step also chooses 1 and silently replaces it; sparse explicit steps have the same problem. Choose one greater than the maximum existing step for this metric.

Medium severity Guard missing run start before RDF trigger lookup

rec/​provenance.py:218

When a run has no qualified start, start is None; in rdflib, g.value(None, PROV.entity) searches for a subject with that predicate rather than returning None. A run with resources but no trigger can therefore deserialize a prov:Usage node as its trigger, breaking the RDF/record round trip. Guard both lookups with if start.

Comment thread rec/run.py
Comment on lines +208 to +211
except Exception as error:
self._stop_heartbeat()
self._emit_failed()
finally:
for observer in self.observers:
observer.close()

self._emit_failed(error)
return self.result
Comment on lines +35 to +39
self.cursor.execute(
f"""
CREATE TABLE IF NOT EXISTS {table} (
id INT AUTO_INCREMENT PRIMARY KEY,
run_id VARCHAR(255) NOT NULL UNIQUE,
- The heartbeat stops in a finally around the hooks and main, so SystemExit no longer leaves its non-daemon thread running
- MariaDBStore.number had no caller; the id column stays to order runs
- Only a new run can be queued and only a queued run cancelled, so a finished run cannot be queued and run again, and cancelling an unknown id creates nothing
- Run queues and cancels only once its stores accept, as it already started
- A record with a value JSON cannot keep is refused for every store, so a result or metric no longer passes in one store and fails in another
- add_agent accepts only PROV-O's agent classes
- MariaDBStore serializes its threads on its one connection; the observer no longer locks
- Queueing, starting, cancelling and ending hold every store's record at once, so a refusal by one store leaves all unchanged; observers sharing a store are refused
- Ending a run and its heartbeat require it in progress
- The observer keeps lists in one order and refuses a change JSON or PROV would not give back unchanged: lossy JSON values, NaN, empty strings, an entity logged twice
- A file is generated once per run (PROV-Constraints generation-uniqueness)
- provenance.record reads only the requested run, and a file's IRI includes its root
- The heartbeat is a daemon thread, so one stuck on a store cannot keep the process alive
- FileStore.save is private to edit
- README states the multi-store limits: no two-phase commit, and opposite store orders can deadlock
- graph() types a run prov-ext:Execution and oslc_auto:AutomationResult whatever its state; a started run adds its start time and association
- A queued or cancelled run conforms with no start time, association or usage
- The shapes refuse a run without a verdict, a queued run with a start time, and a run that is not an execution or not an automation result
- README describes the run's types

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants