Executors

Executors are a contrib invention for implementing backend behavior behind an AdapterBase adapter. Core dispatches only to an adapter executable; it neither discovers executors nor imports ExecutorBase.

Registry, discovery, and lowering

Executor lookup uses (MyExecutor.adapter, MyExecutor.name), not the entry-point key. The authoring name can differ from the resolved target: batch under lambda resolves to the deployed lambda_uri and dml-lambda-adapter.

[project.entry-points."daggerml.contrib.executors"]
example_backend = "my_package.executor:ExampleExecutor"
from daggerml.contrib.status import status


executor_keys = {item["key"] for item in status()["executors"]}
assert {"local:script", "local:docker", "local:ssh"} <= executor_keys

Dispatch and lifecycle

ExecutorBase creates a fresh instance for each call. Durable continuation must therefore be returned as state, not retained on the instance. Repeated invoke requests perform polling; there is no wire poll operation.

from daggerml import Runnable, Uri
from daggerml.contrib.executors import ExecutorBase


class ExampleExecutor(ExecutorBase):
    name = "example"
    adapter = "example"

    @classmethod
    def resolve_runnable(cls, uri, kwargs, sub):
        return Runnable(target=Uri(uri), kwargs=kwargs, sub=sub, adapter="example-adapter")

    def start(self, cache_key, execution_id, runnable, remote, scratch_uri):
        assert remote["root"] == "s3://example/root"
        return {"status": "retry", "adapter_state": {"job": "1"}, "error": None}

    def poll(self, cache_key, execution_id, runnable, state, remote, scratch_uri):
        return {"status": "retry", "adapter_state": state, "error": None}


payload = {
    "operation": "invoke", "cache_key": "cache", "execution_id": "execution",
    "remote": {"root": "s3://example/root"}, "runnable": {"target": {"uri": "example"}},
    "adapter_state": None, "scratch_uri": "s3://example/scratch",
}
assert ExampleExecutor.handle(**payload)["adapter_state"] == {"job": "1"}
assert ExampleExecutor.handle(**{**payload, "adapter_state": {"job": "1"}})["status"] == "retry"

This illustrates resolve-and-dispatch, not a complete deployable backend. Later calls may come from different processes or repositories; continuation must support safe status checks rather than repeated launches.

The runtime owns result publication and lifecycle/cache updates. Required teardown belongs in cleanup, not only terminal polling: publication can stop further invokes. Cleanup must not alter the result, and returning a result does not wait for cleanup retries.

Return cancelled only after confirmed teardown, not merely an accepted termination request. The runtime owns the transition to canceled; retain state and retry while teardown is incomplete.

Backend implementation and built-ins

Script workers receive function source plus explicit extra_objs and post_lines, not the authoring module’s imports or globals. Put dependencies inside the function or inject them explicitly.

Script and Docker handles belong to their launching user and host. Foreign callers retry without touching local resources; hosts and containers must have distinct hostnames. Ownership is not authentication or automatic failover.

Docker and Batch own wrapper resources independently of their nested adapter’s outcome. Their nested --poll driver retains child continuation through cleanup. SSH forwards continuation to its destination; Batch has no local ownership restriction.

Use the supplied remote scratch URI for detached input/output. Runtime records own execution coordination; S3Store under remote.root/data/ is for artifacts.

Docker resource retention

Docker retains completed containers and loaded images; cancellation stops work without deleting them. Use dml contrib docker-gc --dry-run to preview cleanup. Collection defaults to DML-owned resources; --all also includes other users’ resources on the selected daemon. See --help for retention options and Sharp bits for archive tag collisions.

Image age means creation time, not last use. Keep container retention longer than polling delays: collecting a completed container removes its local status handle.

On Linux, dml contrib install-docker-gc enables an hourly systemd user timer. It captures the installer’s Docker/cache settings and Python environment; keep that environment available and reinstall to change settings. The user manager must remain running for scheduled collection. On other Unix systems, schedule dml contrib docker-gc with cron or launchd.

Batch isolation

AWS_EC2_METADATA_DISABLED is not a sandbox: job code can unset it or request metadata directly. EC2-backed Batch operators must block container access to host IMDS, including IPv6 when enabled. For Docker bridge networking, install these rules before admitting work and persist them across reboot:

iptables -I DOCKER-USER 1 -d 169.254.169.254/32 -j DROP
ip6tables -I DOCKER-USER 1 -d fd00:ec2::254/128 -j DROP

Other network modes need the corresponding ECS IMDS block. IMDSv2 alone is not container isolation. Preserve ECS task credentials at 169.254.170.2, scope the task role to required remote/scratch prefixes, and restrict iam:PassRole to that role. Do not use host networking or privileged containers for untrusted work. EC2-backed containers share a kernel; mutually untrusted workloads need separate compute boundaries.

Moto verifies control-plane requests and S3 handoff, not container execution, IAM enforcement, or network isolation. Verify IPv4/IPv6 IMDS requests fail and task-role S3 access succeeds inside a real job before claiming isolation. See AWS’s task role isolation guidance and IMDS credential provider settings.