Skip to content

feat(coordinator)!: plan and run reads through the list of sources - #659

Merged
karhu merged 4 commits into
tnet-157/10-build-sourcesfrom
tnet-157/11-read-coordinator
Nov 10, 2026
Merged

karhu merged 4 commits into
tnet-157/10-build-sourcesfrom
tnet-157/11-read-coordinator

Conversation

@karhu

@karhu karhu commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

The Stack

This stack covers refactoring the internal logic of how releases are resolved during read, download & build, in particular also in the context of parent releases / parent datasets. On top of that it refactors the public TimeNet APIs and CLI interfaces around these actions, to streamline the behavior across them.

This PR

Adds a coordinator for reads, with plan_read() and read_release(). The next PR moves the client onto it.

Details

A build plans and runs through one coordinator since #652, and it gets its parents from an ordered list of sources since #658 (TNET-157). A read still has its own code path in the client. This PR gives reads a coordinator of their own. The next PR moves the client onto it.

  • timenet.coordinator.read has plan_read() and read_release(). plan_read() returns the plan of a read without running it. read_release() runs the plan and opens the release with its payload sources.
  • A read opens a release where it is. The first source that provides the release gives a ReadStep, and the read copies nothing. Each payload source resolves on its own through the same list, at its exact version. So a release and its payload sources can come from different registries.
  • If only InstalledConnectors() provides a release, the plan builds it into the store, as a build does. A copy or a build of a release replaces a read of it, and the read then opens the release from the store. This also holds when a build in the same plan needs a release that the read found in a registry: the release goes into the store, and the read opens it there.
  • Reads and builds share one planner. A read is a fourth outcome of _Graph.visit(), next to no step, a copy and a build. visit() takes needed_in_local_registry. It is true for builds and downloads, which need a complete copy in the store, and false for reads, where a source that serves the release is enough.
  • ReadStep(release, registry, depends_on) joins CopyStep and BuildStep in the Step union. run_plan() has an arm for it that runs nothing.
  • BaseRegistry gets open_release(), which opens one exact release without its payload sources. It can take the manifest from the caller. So a remote read fetches each manifest once.
  • open_version() goes away from every registry, and so does dependencies.open_cached_or_remote(). open_release() and download_release() handle one release without its payload sources, and copy_release() copies a release with them. A read of a release with its payload sources goes through the read coordinator.
  • The read and the builds in its plan share one walk, with one set of answers and manifests.
  • Size: This PR has about 1490 changed lines: about 740 in the source and 750 in tests. The cache of answers and manifests moves out of the build planner into one class that builds and reads share.

Design decisions

  • Before any step runs, the plan checks each file size of each release that it reads from a local registry. A damaged copy in the store stops the read. The read never falls through to a later source, and it never builds over the damage. The check runs after the walk, so a copy that a build in the plan needs can first replace a damaged copy that the read found.
  • A read can need one release twice. For example, a child reads P1 and P2, and the card of P1 declares P2. Each release is one node of the plan, with the pins of each reader. If two readers pin one file differently, no build can match both, and the plan raises TimeFFormatError.
  • A cycle of payload sources through the requested release names that release first.
  • The plan checks the pins of a payload source that the store already holds before any step runs.
  • The plan builds a release with the InstalledBuilder that its lookup read, so it reads the card of each connector once.

Breaking changes

  • BaseRegistry has a new abstract method, open_release(). A registry of another package must implement it.
  • The abstract LazyReadRegistry._open_remote_version() takes an optional manifest.
  • BaseRegistry.open_version() is gone from every registry. To read a release with its payload sources from one registry, use the client, for example TimeNet(registry).open(). From feat(client)!: take an ordered list of sources #661 on, the call is TimeNet(sources=[registry]).open().

Stack

Changes later in the stack:

Merge message

feat(coordinator)!: plan and run reads through the list of sources

plan_read() and read_release() plan and run a read with the planner of
the builds. A read opens each release where the first source provides it
and copies nothing.

- Each payload source resolves on its own at its exact version, so a
  release and its payload sources can come from different registries.
- If only the installed connectors provide a release, the plan builds it
  into the local registry and the read opens it there.
- A damaged copy in the local registry stops the read, unless a copy in
  the same plan replaces it. The read never builds over the damage.
- BaseRegistry.open_release() opens one exact release without its
  payload sources.

BREAKING CHANGE: BaseRegistry gets the abstract open_release(), and
LazyReadRegistry._open_remote_version() takes an optional manifest.

BREAKING CHANGE: BaseRegistry.open_version() is gone. Read through the
client. open_cached_or_remote() becomes open_cached_or_remote_release()
and attaches no payload sources.

@karhu
karhu added this pull request to stack #650 November 4, 2026 13:14
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from c6783de to fd90c6e Compare November 4, 2026 13:16
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from fd90c6e to e1df089 Compare November 4, 2026 13:34
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from e1df089 to f724063 Compare November 4, 2026 14:58
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from f724063 to 33b2ecd Compare November 4, 2026 15:27
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch 2 times, most recently from 2d6a1a0 to 7fc9277 Compare November 4, 2026 20:02
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch 2 times, most recently from 12542d4 to fde254d Compare November 7, 2026 16:27

@TimoStoff TimoStoff left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

alll greeen!

@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from fde254d to e7927e7 Compare November 7, 2026 17:40
@karhu

karhu commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

re top level comment:
Agreed, and done that way. _ReadPlanner is gone with its merge logic.
A read is now a fourth outcome, next to no step, a copy and a build.
Instead of need: Literal["read", "build"], visit() takes needed_in_local_registry: bool, because a download needs a local copy just like a build.

@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from e7927e7 to c86b94d Compare November 7, 2026 22:40
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from c86b94d to 38ecf35 Compare November 9, 2026 13:56
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from 38ecf35 to 600e64c Compare November 10, 2026 14:39

@claude claude Bot 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.

One blocking issue that is not tied to a single line is posted as a comment on its whole file.

@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from 600e64c to 77877e0 Compare November 10, 2026 15:50
@karhu
karhu removed this pull request from stack #699 November 10, 2026 15:58

@claude claude Bot 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.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from 77877e0 to 3a788f2 Compare November 10, 2026 16:24
@karhu
karhu added this pull request to stack #772 November 10, 2026 16:24

@claude claude Bot 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.

Code review found no issues

No high-confidence issues detected in this change.

A read now has its own coordinator, in timenet.coordinator.read.
plan_read() returns the plan of a read without running it.
read_release() runs the plan and opens the release with its payload
sources. The client uses read_release() only for a remote registry so
far.

A read opens a release where it is. The first source that provides the
release gives a ReadStep, and the read copies nothing. Each payload
source resolves on its own through the same list, at its exact
version. So a release and its payload sources can come from different
registries. The pins of the release must match each payload source.

The read and the build share one planner. A read is a fourth outcome
of the walk of the build planner, next to no step, a copy and a build.
The walk takes needed_in_local_registry: a build needs a complete copy
in the store, and a read needs only a source that serves the release.
If only InstalledConnectors() provides a release, the walk plans its
build into the store, with the whole list as the sources of its
parents. If a build also needs a release that the read found in a
registry, the release goes into the store, and the read opens it
there. A read that needs a build without a store raises
TimeFValidationError.

Before any step runs, the plan checks the pins of each payload source.
Then it checks the size of each file of each release that it reads
from a local registry. A copy that a build needs can still repair a
damaged copy that the read found first. A damaged copy in the store
also fails a read that no registry serves. So the read never falls
through to a later source and never builds over the damage.

The plan and the builds that it plans share one set of answers and
manifests. read_release() opens each release with the manifest that
the plan got. So a remote read fetches each manifest once. The walk
takes the answer for the requested release, so a read of the latest
version does not ask again for the exact version.

BaseRegistry gets open_release(), which opens one exact release
without its payload sources. It takes the manifest if the caller has
it, and it may use that manifest. A local registry checks the size of
each file. A remote registry uses a complete cached copy, else the
remote files. open_cached_or_remote_release() holds that one-release
open. Step is now ReadStep | CopyStep | BuildStep, and run_plan() runs
nothing for a read step. The planner shares Lookups, plan_steps() and
describe_searched_sources() with the read coordinator.

BaseRegistry.open_version() goes away, with its implementations and
open_cached_or_remote(). open_release() and download_release() handle
one release without its payload sources, and copy_release() copies a
release with them. A read of a release with its payload sources goes
through the read coordinator.

A read can need one release twice. For example, a child reads P1 and
P2, and the card of P1 declares P2. The release then has one step, and
its build must match the pins of each reader. If two readers pin one
file of the release as different files, no build can match both, and
the plan raises TimeFFormatError. A cycle through the requested
release names that release first.

The plan builds a release with the InstalledBuilder that its lookup
read, so it reads the card of the connector once. The error for a
missing payload source names the sources in the same way as the error
of the plan, with the version that the installed connector builds.

BREAKING CHANGE: BaseRegistry has a new abstract method,
open_release(). A registry of another package must implement it. The
abstract LazyReadRegistry._open_remote_version() takes an optional
manifest. BaseRegistry.open_version() is gone from every registry. To
read a release with its payload sources from one registry, use
TimeNet(sources=[registry]).open().

Part of TNET-157.
Say only what a caller of the read plan and of open_release() needs to know.

Part of TNET-157.
Shorter docstrings for the code that this change adds or changes.

Part of TNET-157.
Drop tests that cannot fail or repeat other tests, and sharpen the rest.

Part of TNET-157.
@karhu
karhu force-pushed the tnet-157/11-read-coordinator branch from 3a788f2 to a2a0fd1 Compare November 10, 2026 17:41
@karhu
karhu merged commit 4cacbd7 into main Nov 10, 2026
4 of 8 checks passed
@karhu
karhu deleted the tnet-157/11-read-coordinator branch November 10, 2026 18:24
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