Skip to content

Feat: registry (backends / transports / schedulers) - #28

Open
Alexander-Auer wants to merge 44 commits into
DIRACGrid:mainfrom
Alexander-Auer:stack/registry
Open

Alexander-Auer wants to merge 44 commits into
DIRACGrid:mainfrom
Alexander-Auer:stack/registry

Conversation

@Alexander-Auer

Copy link
Copy Markdown
Contributor

feat: backend configuration and a closed registry

Part of #18.

Important

Depends on #27. Merge after #27 (PR 2 of 4; the full order is listed in #27).
Until #27 is merged, this diff also shows the earlier PRs. This PR's own change is one commit:
f90cdd4

A consumer can now say which backend it wants as plain data, and get a backend for it:

from intercede import registry

config = {"kind": "batch", "transport": "ssh", "scheduler": "slurm",
          "host": "batch.example.org", "user": "dirac",
          "known_hosts": "batch.example.org ssh-ed25519 AAAA..."}

async with registry.backend(config) as ce:
    await ce.submit(spec, copies)

Configuration: one flat public model

BackendConfig is the only public configuration class. The consumer uses the same field names for every backend, and never needs to know which classes interCEde uses inside:

kind applies
arc, htcondor-ce endpoint, auth (token, proxy)
cloud endpoint, driver, vm_image, vm_flavor, driver_options, vm_networks, vm_ssh_key, vm_max_lifetime_s
batch + local scheduler
batch + ssh scheduler, host, user, known_hosts, port, jump_host, connect_timeout_s, auth (key, password or agent)
all but cloud queue, container_launcher, max_output_bytes, max_output_files, transfer_timeout_s

The three backend families of IC-ADR-001 §4 are covered:

  • monolithic: arc, htcondor-ce
  • provider: cloud
  • composed: batch, a transport combined with a scheduler

How it is checked. Internally, resolve() turns the flat model into the settings of exactly one backend (_backends/config.py), and the model calls it while it is being built. So mistakes are reported immediately, in the consumer's own field names:

host: does not apply to kind='arc'
user: required for kind='batch' with transport='ssh'; known_hosts: required for ...

Rules:

  • Unknown fields and typos are refused, not ignored.
  • There is nowhere to put a secret, as IC-ADR-003 requires. driver_options additionally refuses secret-looking keys.
  • SSH requires known_hosts, because host key verification is not optional.
  • An HTCondor-CE always needs a token.
  • Limits must be positive.
  • A container_launcher must contain an {image} placeholder.

Registry: closed, lazy, checked

  • Closed tables. _BACKENDS, _TRANSPORTS and _SCHEDULERS map a name to "module:Class" and live in interCEde itself, as dictionaries rather than entry points, so nothing outside the library can add to them (IC-ADR-001 Rejected Ideas).
  • Lazy imports. A class is imported only when a configuration asks for it, and cached. A test shows that import intercede imports no backend.
  • Fails fast. Every registered class provides from_settings(...). The registry builds the object and checks it with isinstance against JobBackend, Transport or Scheduler. A missing module, a missing class, a missing from_settings, or an object that breaks its protocol is reported as a RegistryError at resolution, never at the first call.
  • Composition. For batch, the transport is built from its own settings and the scheduler from the batch settings, then both are passed to the batch backend. So ssh+slurm and local+slurm share one class.
  • registry.available() lists what a configuration can ask for in this version, for tooling.

Errors. ConfigurationError means the configuration is invalid. The new RegistryError means the configuration is valid, but this version does not ship what it asks for, or an entry is broken.

The tables currently contain only the internal fake, which the conformance suite now builds through the registry. Each real backend from issues #19 to #23 adds one line plus a from_settings. Until then, valid real configurations correctly report, for example, backend 'arc' is not available in this version of interCEde.

Differences from issue #18 and the ADR

  • No entry points, no plugins and no override precedence. Issue [Feature]: Typed registry over entry points (backends / transports / schedulers) #18 predates the revised IC-ADR-001, which makes the backend set closed. The tables are plain dictionaries instead.
  • One flat public configuration model instead of public per-family models. ADR-001 §5 sketches public per-kind models. This follows the flat shape of the ADR-003 examples ({"kind": "batch", "transport": "ssh", "scheduler": "slurm", "host": ...}) and keeps the per-family settings internal.
  • Only kinds that are planned. lsf, sge, oar and torque are left out for now. Adding a kind later is non-breaking.

Tests

  • tests/unit/test_config.py: every family, required and refused fields, auth values, limits, and the error messages
  • tests/unit/test_registry.py: resolution per family, composition, broken entries, caching, available(), lazy imports, and that calls on the returned object reach the registered class
  • tests/unit/_registry_dummies.py: stand-in classes the tests register

163 tests pass on Python 3.11 and 3.13, and pre-commit, including strict mypy, passes.

alexander and others added 30 commits October 2, 2026 16:54
* feat: Models the core contract needs

* Feat: Protocols and Exceptions added for the core contract

* Feat: Private protocols for the BatchBackend

* feat: a fake backend to run unit tests against

* fix: some code cleanup in the helper functions

* feat: setup for the unit test suite

* chore: style fix

* chore: styling, unit test fixture

* chore: single letter var `h` refering to hanlde changed to `handle`

* fix: unused parameter in `test_injected_failure_is_reported_per_job()` removed

* feat: Models the core contract needs

* Feat: Protocols and Exceptions added for the core contract

* Feat: Private protocols for the BatchBackend

* feat: a fake backend to run unit tests against

* fix: some code cleanup in the helper functions

* feat: setup for the unit test suite

* chore: style fix

* chore: styling, unit test fixture

* chore: single letter var `h` refering to hanlde changed to `handle`

* fix: unused parameter in `test_injected_failure_is_reported_per_job()` removed

* fix: made the OutputSpec optional as it is not suppoerted by all and a consumer should choose to use it or not

---------

Co-authored-by: alexander <alexander.auer@cern.ch>
alexander and others added 4 commits October 9, 2026 16:13
Add BackendConfig, one flat public model a consumer uses to describe any
backend (kind, transport, scheduler, endpoint, host, ...). It is resolved
internally into the settings of exactly one backend family, so a field that
does not apply to the chosen kind is refused by name instead of ignored.

Add the registry: registry.backend(config) builds the backend a
configuration describes, from closed name -> "module:Class" tables. Entries
are imported lazily and every built object is checked against its protocol,
so a broken entry fails at resolution with a RegistryError.

Part of DIRACGrid#18.

Co-authored-by: alexander <alexander.auer@cern.ch>
@Alexander-Auer
Alexander-Auer marked this pull request as ready for review October 20, 2026 14:53
@Alexander-Auer
Alexander-Auer marked this pull request as draft October 20, 2026 14:53
@Alexander-Auer Alexander-Auer self-assigned this Oct 20, 2026
Alexander-Auer and others added 10 commits October 23, 2026 15:16
Drop tests of pydantic and Python behaviour (immutability, defaults, hashing) and keep the rules interCEde itself adds.
One test per rule, no tests of what pydantic or a closed table already guarantees.
Move the API reference to docs/user/reference/api.md, link the user and developer documentation from the index, and stop referring to the ADRs there.
- Google-style docstrings for the public and batch protocols, with arguments, returns and raises.
- get_diagnostics takes a filename (a file in the job's working directory; None reads stdout) instead of a generic name; Diagnostics.filename matches.
- An input name may appear once in a SubmissionSpec: two sources for one file are refused when the spec is built.
- A copy with a secret named like an input is refused at submit with SpecificationRejectedError; SubmissionSpec.clashes_with() lets every backend apply the same rule.
- No references to the ADRs in code or docstrings: they are not part of the source and can be superseded.
Follows the review of the core contract: the ADRs are not part of the source and can be superseded. The registry's docstrings also say what they return.
- Google-style docstrings with Args, Returns and Raises for resolve() and every registry and configuration helper, and docstrings for the settings classes; ruff now checks docstrings against the Google convention.
- Remove vm_max_lifetime_s: removing VMs after a lifetime is the consumer's policy, not interCEde's.
- Import Callable from collections.abc, like the rest of the code.
@Alexander-Auer
Alexander-Auer marked this pull request as ready for review October 26, 2026 09:10
@Alexander-Auer
Alexander-Auer requested a review from aldbr October 26, 2026 09:10

@aldbr aldbr 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.

A few comments, most are minor.
I just have a question around BackendConfig: I would lean towards different public interfaces rather than 1 big interface. But no strong opinion about it, we can discuss

Comment thread src/intercede/registry.py
}


def backend(config: BackendConfig | Mapping[str, Any]) -> JobBackend:

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.

What about naming it get_backend()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that is probably a bit nicer yes

Comment thread src/intercede/registry.py
try:
config = BackendConfig.model_validate(config)
except ValidationError as exc:
raise ConfigurationError(str(exc)) from exc

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.

Is there any risk of leaking secrets?
I know it shouldn't be the case, but one could potentially misunderstand the API and try to add a secret

Example:

from intercede import registry

config = read_from_file()
# {"kind": "batch", ...,  "password": "toto4"} ⚠️ 

async with registry.backend(config) as ce:
    await ce.submit(spec, copies)
1 validation error for BackendConfig
  password
    Extra inputs are not permitted [type=extra_forbidden, input_value='toto4', input_type=str]

What do you think?
Can you check if there is any easy way of just displaying expected values (declared in BackendConfig and scalar)?

Comment thread src/intercede/registry.py
RegistryError: The configuration is valid, but this version of
interCEde does not provide what it asks for.
"""
if not isinstance(config, BackendConfig):

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.

Can you see whether this raises an error please?

base = BackendConfig(
    kind="batch",
    transport="ssh",
    scheduler="slurm",
    host="login.example.org",
    known_hosts="login.example.org ssh-ed25519 AAAA…"
)
local = base.model_copy(update={"transport": "local"})   # forgot to clear host/known_hosts

registry.backend(local)

Comment thread src/intercede/config.py
Comment on lines +50 to +65
============================ ==========================================
``kind`` fields
============================ ==========================================
``arc``, ``htcondor-ce`` ``endpoint``, ``auth`` (``token``, ``proxy``)
``cloud`` ``endpoint``, ``driver``, ``vm_image``,
``vm_flavor``, ``driver_options``,
``vm_networks``, ``vm_ssh_key``
``batch`` + ``local`` ``scheduler``
``batch`` + ``ssh`` ``scheduler``, ``host``, ``user``,
``known_hosts``, ``port``, ``jump_host``,
``connect_timeout_s``, ``auth`` (one of
``key``, ``password``, ``agent``)
all but ``cloud`` ``queue``, ``container_launcher``,
``max_output_bytes``, ``max_output_files``,
``transfer_timeout_s``
============================ ==========================================

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.

This has the potential of creating discrepancies if any of these fields is updated.
It would be probably be useful but would be better if it's automatically generated from the code somewhere in the documentation for instance.
See https://github.com/DIRACGrid/diracx/blob/3223011c5b8cc1d0a77636470ac40e76c19e9e36/pixi.toml#L246 in diracx for instance

Comment thread tests/unit/test_config.py
from pydantic import ValidationError

from intercede import BackendConfig
from intercede._backends.config import (

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.

Note: ideally, we want unit tests to be focused on the public API to avoid changing the unit tests for a minor change in a private function.

Unit tests on private components can be interesting if something is complex to test though

Comment on lines +20 to +21
LIMIT_FIELDS = ("max_output_bytes", "max_output_files", "transfer_timeout_s")
SSH_FIELDS = ("host", "port", "user", "known_hosts", "jump_host", "connect_timeout_s")

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.

These fields are defined twice (here and in the pydantic models): this can introduce discrepancies.
Ideally you get them from the models like:

LIMIT_FIELDS = tuple(Limits.model_fields)
SSH_FIELDS = tuple(name for name in SSHTransportSettings.model_fields if name not in ("kind", "auth"))

Comment on lines +86 to +87
_SECRET_NAMES = ("secret", "password", "token", "passphrase")
_SECRET_EXACT = ("key", "api_key", "access_key", "private_key")

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.

Out of curiosity, did you choose these names yourself or is it based on any guidelines?

Comment thread src/intercede/registry.py
Comment on lines +74 to +78
try:
config = BackendConfig.model_validate(config)
except ValidationError as exc:
raise ConfigurationError(str(exc)) from exc
return _build(resolve(config))

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.

Here resolve() is called twice: 1st by _fits_the_backend calling resolve() and then here. Is that expected?

Comment thread src/intercede/config.py
__all__ = ["BackendConfig"]


class BackendConfig(_Model):

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.

Do we need BackendConfig?

Sounds like a potential source of discrepancy here also.

What about:

BackendConfig = Annotated[
    Union[
        Annotated[ARCConfig, Tag("arc")],
        Annotated[CloudConfig, Tag("cloud")],
        Annotated[BatchLocalConfig, Tag("batch+local")],
        Annotated[BatchSSHConfig, Tag("batch+ssh")]
    ],
    Discriminator(lambda v: f"batch+{v.get('transport')}" if v.get("kind") == "batch" else v.get("kind")),
]

Where ...Config would correspond to your ...Settings classes.

The user would get:

  • Generated reference docs: one page per backend with exactly its fields (the current vesion would give one page of ~25 optional fields).
  • Typed code
  • If we ever publish JSON schema, editors could suggest only the fields valid for the chosen kind.

But the flat model is also convenient:

  • One interface, and internal classes can be reorganised without breaking the API.
  • Load errors read slightly better today: host: does not apply to kind='arc' vs arc.host: Extra inputs are not permitted.

Do you have any opinion?

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